/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* global define, module, require, exports */

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery',
                'Slick',
                'nf.Common',
                'nf.Client',
                'nf.ErrorHandler'],
            function ($, Slick, nfCommon, nfClient, nfErrorHandler) {
                return (nf.UsersTable = factory($, Slick, nfCommon, nfClient, nfErrorHandler));
            });
    } else if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = (nf.UsersTable =
            factory(require('jquery'),
                require('Slick'),
                require('nf.Common'),
                require('nf.Client'),
                require('nf.ErrorHandler')));
    } else {
        nf.UsersTable = factory(root.$,
            root.Slick,
            root.nf.Common,
            root.nf.Client,
            root.nf.ErrorHandler);
    }
}(this, function ($, Slick, nfCommon, nfClient, nfErrorHandler) {
    'use strict';

    var isDisconnectionAcknowledged = false;

    /**
     * Configuration object used to hold a number of configuration items.
     */
    var config = {
        urls: {
            users: '../nifi-api/tenants/users',
            userGroups: '../nifi-api/tenants/user-groups'
        }
    };

    /**
     * Return user entity from the dataset of the users table
     * @param userId 
     * @returns user
     */
    var getUserById = function (userId) {
        var usersGrid = $('#users-table').data('gridInstance');
        var usersData = usersGrid.getData();
        return usersData.getItemById('user_' + userId);
    }

    /**
     * Return group entity from the dataset of the users table
     * @param groupId 
     * @returns group
     */
    var getGroupById = function (groupId) {
        var usersGrid = $('#users-table').data('gridInstance');
        var usersData = usersGrid.getData();
        return usersData.getItemById('group_' + groupId);
    }

    /**
     * Selects a row in the users table
     * @param {user | group} type 
     * @param id 
     */
    var selectRow = function (type, id) {
        var usersGrid = $('#users-table').data('gridInstance');
        var usersData = usersGrid.getData();
        var row = usersData.getRowById(type + '_' + id);
        usersGrid.setSelectedRows([row]);
        usersGrid.scrollRowIntoView(row);
    }

    var initUserDeleteDialog = function () {
        $('#user-delete-dialog').modal({
            headerText: 'Delete Account',
            buttons: [{
                buttonText: 'Delete',
                color: {
                    base: '#728E9B',
                    hover: '#004849',
                    text: '#ffffff'
                },
                handler: {
                    click: function () {
                        // delete dialog contains the extended id
                        var extendedUserId = $('#user-extended-id-delete-dialog').val();

                        // get the user
                        var usersGrid = $('#users-table').data('gridInstance');
                        var usersData = usersGrid.getData();
                        var user = usersData.getItemById(extendedUserId);
                        var revision = nfClient.getRevision(user);

                        // update the user
                        $.ajax({
                            type: 'DELETE',
                            url: user.uri + '?' + $.param($.extend({
                                'disconnectedNodeAcknowledged': isDisconnectionAcknowledged
                            }, revision)),
                            dataType: 'json'
                        }).done(function () {
                            nfUsersTable.loadUsersTable();
                        }).fail(nfErrorHandler.handleAjaxError);

                        // hide the dialog
                        $('#user-delete-dialog').modal('hide');
                    }
                }
            }, {
                buttonText: 'Cancel',
                color: {
                    base: '#E3E8EB',
                    hover: '#C7D2D7',
                    text: '#004849'
                },
                handler: {
                    click: function () {
                        $('#user-delete-dialog').modal('hide');
                    }
                }
            }],
            handler: {
                close: function () {
                    // clear the current user
                    $('#user-extended-id-delete-dialog').val('');
                    $('#user-name-delete-dialog').text('');
                }
            }
        });
    };

    /**
     * Gets the currently selected groups.
     *
     * @returns {Array} groups
     */
    var getSelectedGroups = function () {
        var selectedGroups = [];
        $('#available-groups div.group-check').filter(function () {
            return $(this).hasClass('checkbox-checked');
        }).each(function () {
            var id = $(this).next('span.group-id').text();
            selectedGroups.push({
                'id': id
            });
        });
        return selectedGroups;
    };

    /**
     * Gets the currently selected users.
     *
     * @returns {Array} users
     */
    var getSelectedUsers = function () {
        var selectedUsers = [];
        $('#available-users div.user-check').filter(function () {
            return $(this).hasClass('checkbox-checked');
        }).each(function () {
            var id = $(this).next('span.user-id').text();
            selectedUsers.push({
                'id': id
            });
        });
        return selectedUsers;
    };

    /**
     * Adds the specified user to the specified group.
     *
     * @param groupEntity
     * @param userEntity
     * @returns xhr
     */
    var addUserToGroup = function (groupEntity, userEntity) {
        var groupMembers = [];

        // get all the current users
        $.each(groupEntity.component.users, function (_, member) {
            groupMembers.push({
                'id': member.id
            });
        });

        // add the new user
        groupMembers.push({
            'id': userEntity.id
        });

        // build the request entity
        var updatedGroupEntity = {
            'revision': nfClient.getRevision(groupEntity),
            'disconnectedNodeAcknowledged': isDisconnectionAcknowledged,
            'component': $.extend({}, groupEntity.component, {
                'users': groupMembers
            })
        };

        // update the group
        return $.ajax({
            type: 'PUT',
            url: groupEntity.uri,
            data: JSON.stringify(updatedGroupEntity),
            dataType: 'json',
            contentType: 'application/json'
        });
    };

    /**
     * Adds the specified user to the specified group.
     *
     * @param groupEntity
     * @param userEntity
     * @returns xhr
     */
    var removeUserFromGroup = function (groupEntity, userEntity) {
        var groupMembers = [];

        // get all the current users
        $.each(groupEntity.component.users, function (_, member) {
            // do not include the specified user
            if (member.id !== userEntity.id) {
                groupMembers.push({
                    'id': member.id
                });
            }
        });

        // build the request entity
        var updatedGroupEntity = {
            'revision': nfClient.getRevision(groupEntity),
            'disconnectedNodeAcknowledged': isDisconnectionAcknowledged,
            'component': $.extend({}, groupEntity.component, {
                'users': groupMembers
            })
        };

        // update the group
        return $.ajax({
            type: 'PUT',
            url: groupEntity.uri,
            data: JSON.stringify(updatedGroupEntity),
            dataType: 'json',
            contentType: 'application/json'
        });
    };

    /**
     * Creates the specified user.
     *
     * @param newUserEntity
     * @param selectedGroups
     */
    var createUser = function (newUserEntity, selectedGroups) {

        // create the user
        var userXhr = $.ajax({
            type: 'POST',
            url: config.urls.users,
            data: JSON.stringify(newUserEntity),
            dataType: 'json',
            contentType: 'application/json'
        });

        // if the user was successfully created
        userXhr.done(function (userEntity) {
            $('#user-dialog').modal('hide');

            var xhrs = [];
            $.each(selectedGroups, function (_, selectedGroup) {
                var groupEntity = getGroupById(selectedGroup.id)
                xhrs.push(addUserToGroup(groupEntity, userEntity));
            });

            $.when.apply(window, xhrs).always(function () {
                nfUsersTable.loadUsersTable().done(function () {
                    // select the new user
                    selectRow('user', userEntity.id);
                });
            }).fail(nfErrorHandler.handleAjaxError);
        }).fail(nfErrorHandler.handleConfigurationUpdateAjaxError);
    };

    /**
     * Updates the specified user.
     *
     * @param userId
     * @param userIdentity
     * @param selectedGroups
     */
    var updateUser = function (userId, userIdentity, selectedGroups) {
        var userEntity = getUserById(userId);

        var updatedUserEntity = {
            'revision': nfClient.getRevision(userEntity),
            'disconnectedNodeAcknowledged': isDisconnectionAcknowledged,
            'component': {
                'id': userId,
                'identity': userIdentity
            }
        };

        // update the user
        var userXhr = $.ajax({
            type: 'PUT',
            url: userEntity.uri,
            data: JSON.stringify(updatedUserEntity),
            dataType: 'json',
            contentType: 'application/json'
        });

        userXhr.done(function (updatedUserEntity) {

            $('#user-dialog').modal('hide');

            // determine what to add/remove
            var groupsAdded = [];
            var groupsRemoved = [];
            $.each(updatedUserEntity.component.userGroups, function (_, currentGroup) {
                var isSelected = $.grep(selectedGroups, function (group) {
                    return group.id === currentGroup.id;
                });

                // if the current group is not selected, mark it for removed
                if (isSelected.length === 0) {
                    groupsRemoved.push(currentGroup);
                }
            });
            $.each(selectedGroups, function (_, selectedGroup) {
                var isSelected = $.grep(updatedUserEntity.component.userGroups, function (group) {
                    return group.id === selectedGroup.id;
                });

                // if the selected group is not current, mark it for addition
                if (isSelected.length === 0) {
                    groupsAdded.push(selectedGroup);
                }
            });

            // update each group
            var xhrs = [];
            $.each(groupsAdded, function (_, group) {
                var groupEntity = getGroupById(group.id);
                xhrs.push(addUserToGroup(groupEntity, updatedUserEntity))
            });
            $.each(groupsRemoved, function (_, group) {
                var groupEntity = getGroupById(group.id);
                xhrs.push(removeUserFromGroup(groupEntity, updatedUserEntity));
            });

            $.when.apply(window, xhrs).always(function () {
                nfUsersTable.loadUsersTable();
            }).fail(nfErrorHandler.handleAjaxError);
        }).fail(nfErrorHandler.handleConfigurationUpdateAjaxError);
    };

    /**
     * Creates the specified group.
     *
     * @param newGroupEntity
     */
    var createGroup = function (newGroupEntity) {
        // create the group
        $.ajax({
            type: 'POST',
            url: config.urls.userGroups,
            data: JSON.stringify(newGroupEntity),
            dataType: 'json',
            contentType: 'application/json'
        }).done(function (groupEntity) {
            $('#user-dialog').modal('hide');
            nfUsersTable.loadUsersTable().done(function () {
                // select the new group
                selectRow('group', groupEntity.id);
            });
        }).fail(nfErrorHandler.handleConfigurationUpdateAjaxError);
    };

    /**
     * Updates the specified group.
     *
     * @param groupId
     * @param groupIdentity
     * @param selectedUsers
     */
    var updateGroup = function (groupId, groupIdentity, selectedUsers) {
        var groupEntity = getGroupById(groupId);

        var updatedGroupoEntity = {
            'revision': nfClient.getRevision(groupEntity),
            'disconnectedNodeAcknowledged': isDisconnectionAcknowledged,
            'component': {
                'id': groupId,
                'identity': groupIdentity,
                'users': selectedUsers
            }
        };

        // update the user
        $.ajax({
            type: 'PUT',
            url: groupEntity.uri,
            data: JSON.stringify(updatedGroupoEntity),
            dataType: 'json',
            contentType: 'application/json'
        }).done(function (groupEntity) {
            $('#user-dialog').modal('hide');
            nfUsersTable.loadUsersTable();
        }).fail(nfErrorHandler.handleConfigurationUpdateAjaxError);
    };

    /**
     * Initializes the user table.
     */
    var initUserDialog = function () {
        $('#user-dialog').modal({
            headerText: 'User/Group',
            buttons: [{
                buttonText: 'Ok',
                color: {
                    base: '#728E9B',
                    hover: '#004849',
                    text: '#ffffff'
                },
                handler: {
                    click: function () {
                        var userId = $('#user-id-edit-dialog').text();
                        var userIdentity = $('#user-identity-edit-dialog').val();

                        // see if we should create or update this user
                        if ($.trim(userId) === '') {
                            var tenantEntity = {
                                'revision': nfClient.getRevision({
                                    'revision': {
                                        'version': 0
                                    }
                                }),
                                'disconnectedNodeAcknowledged': isDisconnectionAcknowledged
                            };

                            // handle whether it's a user or a group
                            if ($('#individual-radio-button').is(':checked')) {
                                // record the user groups
                                tenantEntity['component'] = {
                                    'identity': userIdentity
                                };

                                createUser(tenantEntity, getSelectedGroups());
                            } else {
                                // record the users
                                tenantEntity['component'] = {
                                    'identity': userIdentity,
                                    'users': getSelectedUsers()
                                };

                                createGroup(tenantEntity);
                            }

                            // update any selected policies
                        } else {
                            // handle whether it's a user or a group
                            if ($('#individual-radio-button').is(':checked')) {
                                updateUser(userId, userIdentity, getSelectedGroups());

                                // update any selected policies
                            } else {
                                updateGroup(userId, userIdentity, getSelectedUsers());

                                // update any selected policies
                            }
                        }

                    }
                }
            }, {
                buttonText: 'Cancel',
                color: {
                    base: '#E3E8EB',
                    hover: '#C7D2D7',
                    text: '#004849'
                },
                handler: {
                    click: function () {
                        $('#user-dialog').modal('hide');
                    }
                }
            }],
            handler: {
                close: function () {
                    // reset the radio button
                    $('#user-dialog input[name="userOrGroup"]').attr('disabled', false);
                    $('#individual-radio-button').prop('checked', true);
                    $('#user-groups').show();
                    $('#group-members').hide();

                    // clear the fields
                    $('#user-id-edit-dialog').text('');
                    $('#user-identity-edit-dialog').val('');
                    $('#available-users').empty()
                    $('#available-groups').empty()
                }
            }
        });

        // listen for changes to the user/group radio
        $('#user-dialog input[name="userOrGroup"]').on('change', function () {
            if ($(this).val() === 'individual') {
                $('#user-groups').show();
                $('#group-members').hide();
            } else {
                $('#user-groups').hide();
                $('#group-members').show();
            }
        });
    };

    /**
     * Initializes the user policies dialog.
     */
    var initUserPoliciesDialog = function () {
        $('#user-policies-dialog').modal({
            headerText: 'User Policies',
            buttons: [{
                buttonText: 'Close',
                color: {
                    base: '#728E9B',
                    hover: '#004849',
                    text: '#ffffff'
                },
                handler: {
                    click: function () {
                        $('#user-policies-dialog').modal('hide');
                    }
                }
            }]
        });
    };

    /**
     * Generates a human readable global policy string.
     *
     * @param policy
     * @returns {string}
     */
    var globalResourceParser = function (policy) {
        return 'Global policy to ' + policy.text;
    };

    /**
     * Generates a human readable policy string for an unknown resource.
     *
     * @param dataContext
     * @returns {string}
     */
    var unknownResourceParser = function (dataContext) {
        return '<span class="unset">Unknown resource ' + nfCommon.escapeHtml(dataContext.component.resource) + '</span>';
    };

    /**
     * Generates a human readable restricted component policy string.
     *
     * @param dataContext
     * @returns {string}
     */
    var restrictedComponentResourceParser = function (dataContext) {
        var resource = dataContext.component.resource;

        if (resource === '/restricted-components') {
            return 'Restricted components regardless of restrictions';
        }

        var subResource = nfCommon.substringAfterFirst(resource, '/restricted-components/');
        return "Restricted components requiring '" + nfCommon.escapeHtml(subResource) + "'";
    };

    /**
     * Generates a human readable component policy string.
     *
     * @param dataContext
     * @returns {string}
     */
    var componentResourceParser = function (dataContext) {
        var resource = dataContext.component.resource;
        var policyLabel = '';

        //determine policy type
        if (resource.startsWith('/policies')) {
            resource = nfCommon.substringAfterFirst(resource, '/policies');
            policyLabel += 'Admin policy for ';
        } else if (resource.startsWith('/data-transfer')) {
            resource = nfCommon.substringAfterFirst(resource, '/data-transfer');
            policyLabel += 'Site to site policy for ';
        } else if (resource.startsWith('/data')) {
            resource = nfCommon.substringAfterFirst(resource, '/data');
            policyLabel += 'Data policy for ';
        } else if (resource.startsWith('/operation')) {
            resource = nfCommon.substringAfterFirst(resource, '/operation');
            policyLabel += 'Operate policy for ';
        } else {
            policyLabel += 'Component policy for ';
        }

        if (resource.startsWith('/processors')) {
            policyLabel += 'processor ';
        } else if (resource.startsWith('/controller-services')) {
            policyLabel += 'controller service ';
        } else if (resource.startsWith('/funnels')) {
            policyLabel += 'funnel ';
        } else if (resource.startsWith('/input-ports')) {
            policyLabel += 'input port ';
        } else if (resource.startsWith('/labels')) {
            policyLabel += 'label ';
        } else if (resource.startsWith('/output-ports')) {
            policyLabel += 'output port ';
        } else if (resource.startsWith('/process-groups')) {
            policyLabel += 'process group ';
        } else if (resource.startsWith('/remote-process-groups')) {
            policyLabel += 'remote process group ';
        } else if (resource.startsWith('/reporting-tasks')) {
            policyLabel += 'reporting task ';
        } else if (resource.startsWith('/parameter-contexts')) {
            policyLabel += 'parameter context '
        }

        if (dataContext.component.componentReference.permissions.canRead === true) {
            policyLabel += '<span style="font-weight: 500">' + nfCommon.escapeHtml(dataContext.component.componentReference.component.name) + '</span>';
        } else {
            policyLabel += '<span class="unset">' + nfCommon.escapeHtml(dataContext.component.componentReference.id) + '</span>'
        }

        return policyLabel;
    };

    /**
     * Initializes the user policies table.
     */
    var initUserPoliciesTable = function () {

        // function for formatting the human readable name of the policy
        var policyDisplayNameFormatter = function (row, cell, value, columnDef, dataContext) {
            // if the user has permission to the policy
            if (dataContext.permissions.canRead === true) {
                return formatPolicy(dataContext);
            } else {
                return '<span class="unset">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
            }
        };

        // function for formatting the actions column
        var actionsFormatter = function (row, cell, value, columnDef, dataContext) {
            var markup = '';

            if (dataContext.permissions.canRead === true) {
                if (nfCommon.isDefinedAndNotNull(dataContext.component.componentReference)) {
                    if (dataContext.component.resource.indexOf('/processors') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/controller-services') >= 0) {
                        //TODO: implement go to for CS
                    } else if (dataContext.component.resource.indexOf('/funnels') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/input-ports') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/labels') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/output-ports') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/process-groups') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-process-group fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/remote-process-groups') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-component fa fa-long-arrow-right" style="float: left;"></div>';
                    } else if (dataContext.component.resource.indexOf('/reporting-tasks') >= 0) {
                        //TODO: implement go to for RT
                    } else if (dataContext.component.resource.indexOf('/parameter-contexts') >= 0) {
                        markup += '<div title="Go To" class="pointer go-to-parameter-context fa fa-long-arrow-right" style="float: left;"></div>';
                    }
                }
            }

            return markup;
        };

        // function for formatting the action column
        var actionFormatter = function (row, cell, value, columnDef, dataContext) {
            var markup = '';

            if (dataContext.permissions.canRead === true) {
                markup += dataContext.component.action;
            }

            return nfCommon.escapeHtml(markup);
        };

        var userPoliciesColumns = [
            {
                id: 'policy',
                name: 'Policy',
                sortable: true,
                resizable: true,
                formatter: policyDisplayNameFormatter,
                width: 150
            },
            {
                id: 'action',
                name: 'Action',
                sortable: true,
                resizable: false,
                formatter: actionFormatter,
                width: 50
            }
        ];

        // add the actions if we're in the shell
        if (top !== window) {
            userPoliciesColumns.push({
                id: 'actions',
                name: '&nbsp;',
                sortable: false,
                resizable: false,
                formatter: actionsFormatter,
                width: 25
            });
        }

        var userPoliciesOptions = {
            autosizeColsMode: Slick.GridAutosizeColsMode.LegacyForceFit,
            enableTextSelectionOnCells: true,
            enableCellNavigation: true,
            enableColumnReorder: false,
            autoEdit: false
        };

        // initialize the dataview
        var userPoliciesData = new Slick.Data.DataView({
            inlineFilters: false
        });
        userPoliciesData.setItems([]);

        // initialize the sort
        userPolicySort({
            columnId: 'policy',
            sortAsc: true
        }, userPoliciesData);

        // initialize the grid
        var userPoliciesGrid = new Slick.Grid('#user-policies-table', userPoliciesData, userPoliciesColumns, userPoliciesOptions);
        userPoliciesGrid.setSelectionModel(new Slick.RowSelectionModel());
        userPoliciesGrid.registerPlugin(new Slick.AutoTooltips());
        userPoliciesGrid.setSortColumn('policy', true);
        userPoliciesGrid.onSort.subscribe(function (e, args) {
            userPolicySort({
                columnId: args.sortCol.id,
                sortAsc: args.sortAsc
            }, userPoliciesData);
        });

        // configure a click listener
        userPoliciesGrid.onClick.subscribe(function (e, args) {
            var target = $(e.target);

            // get the node at this row
            var item = userPoliciesData.getItem(args.row);

            // determine the desired action
            if (userPoliciesGrid.getColumns()[args.cell].id === 'actions') {
                if (target.hasClass('go-to-component')) {
                    parent.$('body').trigger('GoTo:Component', {
                        id: item.component.componentReference.id,
                        parentGroupId: item.component.componentReference.parentGroupId
                    });
                    parent.$('#shell-close-button').click();
                } else if (target.hasClass('go-to-process-group')) {
                    parent.$('body').trigger('GoTo:ProcessGroup', {
                        id: item.component.componentReference.id
                    });
                    parent.$('#shell-close-button').click();
                } else if (target.hasClass('go-to-parameter-context')) {
                    parent.$('body').trigger('GoTo:ParameterContext', {
                        id: item.component.componentReference.id
                    });
                    parent.$('#shell-close-button').click();
                }
            }
        });

        // wire up the dataview to the grid
        userPoliciesData.onRowCountChanged.subscribe(function (e, args) {
            userPoliciesGrid.updateRowCount();
            userPoliciesGrid.render();
        });
        userPoliciesData.onRowsChanged.subscribe(function (e, args) {
            userPoliciesGrid.invalidateRows(args.rows);
            userPoliciesGrid.render();
        });

        // hold onto an instance of the grid
        $('#user-policies-table').data('gridInstance', userPoliciesGrid);
    };

    /**
     * Initializes the processor list.
     */
    var initUsersTable = function (configurableUsersAndGroups) {
        // define the function for filtering the list
        $('#users-filter').keyup(function () {
            applyFilter();
        });

        // filter type
        $('#users-filter-type').combo({
            options: [{
                text: 'by user',
                value: 'identity'
            }],
            select: function (option) {
                applyFilter();
            }
        });

        // function for formatting the user identity
        var identityFormatter = function (row, cell, value, columnDef, dataContext) {
            var markup = '';
            if (dataContext.type === 'group') {
                markup += '<div class="fa fa-users"></div>';
            }

            markup += nfCommon.escapeHtml(dataContext.component.identity);

            return markup;
        };

        // function for formatting the members/groups
        var membersGroupsFormatter = function (row, cell, value, columnDef, dataContext) {
            if (dataContext.type === 'group') {
                return 'Members: <b>' + dataContext.component.users.map(function (user) {
                        return nfCommon.escapeHtml(user.component.identity);
                    }).join('</b>, <b>') + '</b>';
            } else {
                return 'Member of: <b>' + dataContext.component.userGroups.map(function (group) {
                        return nfCommon.escapeHtml(group.component.identity);
                    }).join('</b>, <b>') + '</b>';
            }
        };

        // function for formatting the actions column
        var actionFormatter = function (row, cell, value, columnDef, dataContext) {
            var markup = '';

            // ensure user can modify the user
            if (configurableUsersAndGroups && dataContext.component.configurable === true && nfCommon.canModifyTenants()) {
                markup += '<div title="Edit" class="pointer edit-user fa fa-pencil"></div>';
                markup += '<div title="Remove" class="pointer delete-user fa fa-trash"></div>';
            }

            if (!nfCommon.isEmpty(dataContext.component.accessPolicies)) {
                markup += '<div title="View User Policies" class="pointer view-user-policies fa fa-key" style="margin-left: 3px;"></div>';
            }

            return markup;
        };

        // initialize the users table
        var usersColumns = [
            {
                id: 'identity',
                name: 'User',
                sortable: true,
                resizable: true,
                formatter: identityFormatter
            },
            {
                id: 'membersGroups',
                name: '&nbsp;',
                sortable: true,
                defaultSortAsc: false,
                resizable: true,
                formatter: membersGroupsFormatter
            },
            {
                id: 'actions',
                name: '&nbsp;',
                sortable: false,
                resizable: false,
                formatter: actionFormatter,
                width: 100,
                maxWidth: 100
            }
        ];

        var usersOptions = {
            autosizeColsMode: Slick.GridAutosizeColsMode.LegacyForceFit,
            enableTextSelectionOnCells: true,
            enableCellNavigation: true,
            enableColumnReorder: false,
            autoEdit: false
        };

        // initialize the dataview
        var usersData = new Slick.Data.DataView({
            inlineFilters: false
        });
        usersData.setItems([], 'extendedId');
        usersData.setFilterArgs({
            searchString: getFilterText(),
            property: $('#users-filter-type').combo('getSelectedOption').value
        });
        usersData.setFilter(filter);

        // initialize the sort
        userSort({
            columnId: 'identity',
            sortAsc: true
        }, usersData);

        // initialize the grid
        var usersGrid = new Slick.Grid('#users-table', usersData, usersColumns, usersOptions);
        usersGrid.setSelectionModel(new Slick.RowSelectionModel());
        usersGrid.registerPlugin(new Slick.AutoTooltips());
        usersGrid.setSortColumn('identity', true);
        usersGrid.onSort.subscribe(function (e, args) {
            userSort({
                columnId: args.sortCol.id,
                sortAsc: args.sortAsc
            }, usersData);
        });

        // configure a click listener
        usersGrid.onClick.subscribe(function (e, args) {
            var target = $(e.target);

            // get the node at this row
            var item = usersData.getItem(args.row);

            // determine the desired action
            if (usersGrid.getColumns()[args.cell].id === 'actions') {
                if (target.hasClass('edit-user')) {
                    editUser(item);
                } else if (target.hasClass('view-user-policies')) {
                    viewUserPolicies(item);
                } else if (target.hasClass('delete-user')) {
                    deleteUser(item);
                }
            }
        });

        // wire up the dataview to the grid
        usersData.onRowCountChanged.subscribe(function (e, args) {
            usersGrid.updateRowCount();
            usersGrid.render();

            // update the total number of displayed processors
            $('#displayed-users').text(args.current);
        });
        usersData.onRowsChanged.subscribe(function (e, args) {
            usersGrid.invalidateRows(args.rows);
            usersGrid.render();
        });

        // hold onto an instance of the grid
        $('#users-table').data('gridInstance', usersGrid);

        // initialize the number of displayed items
        $('#displayed-users').text('0');
    };

    /**
     * Sorts the specified data using the specified sort details.
     *
     * @param {object} sortDetails
     * @param {object} data
     */
    var userSort = function (sortDetails, data) {
        // defines a function for sorting
        var comparer = function (a, b) {
            if (a.permissions.canRead && b.permissions.canRead) {
                var aString = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : '';
                var bString = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : '';
                return aString === bString ? 0 : aString > bString ? 1 : -1;
            } else {
                if (!a.permissions.canRead && !b.permissions.canRead) {
                    return 0;
                }
                if (a.permissions.canRead) {
                    return 1;
                } else {
                    return -1;
                }
            }
        };

        // perform the sort
        data.sort(comparer, sortDetails.sortAsc);
    };

    /**
     * Formats the specified policy.
     *
     * @param dataContext
     * @returns {string}
     */
    var formatPolicy = function (dataContext) {
        if (dataContext.component.resource.startsWith('/restricted-components')) {
            // restricted components policy
            return restrictedComponentResourceParser(dataContext);
        } else if (nfCommon.isDefinedAndNotNull(dataContext.component.componentReference)) {
            // not restricted/global policy... check if user has access to the component reference
            return componentResourceParser(dataContext);
        } else {
            // may be a global policy
            var policy = nfCommon.getPolicyTypeListing(nfCommon.substringAfterFirst(dataContext.component.resource, '/'));

            // if known global policy, format it otherwise format as unknown
            if (nfCommon.isDefinedAndNotNull(policy)) {
                return globalResourceParser(policy);
            } else {
                return unknownResourceParser(dataContext);
            }
        }
    };

    /**
     * Sorts the specified data using the specified sort details.
     *
     * @param {object} sortDetails
     * @param {object} data
     */
    var userPolicySort = function (sortDetails, data) {
        // defines a function for sorting
        var comparer = function (a, b) {
            if (a.permissions.canRead && b.permissions.canRead) {
                if (sortDetails.columnId === 'action') {
                    var aString = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : '';
                    var bString = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : '';
                    return aString === bString ? 0 : aString > bString ? 1 : -1;
                } else if (sortDetails.columnId === 'policy') {
                    var aString = '';
                    var bString = '';

                    // if the user has permission to the policy
                    if (a.permissions.canRead === true) {
                        aString = formatPolicy(a);
                    } else {
                        aString = a.id;
                    }

                    // if the user has permission to the policy
                    if (b.permissions.canRead === true) {
                        bString = formatPolicy(b);
                    } else {
                        bString = b.id;
                    }

                    return aString === bString ? 0 : aString > bString ? 1 : -1;
                }
            } else {
                if (!a.permissions.canRead && !b.permissions.canRead) {
                    return 0;
                }
                if (a.permissions.canRead) {
                    return 1;
                } else {
                    return -1;
                }
            }
        };

        // perform the sort
        data.sort(comparer, sortDetails.sortAsc);
    };

    /**
     * Get the text out of the filter field. If the filter field doesn't
     * have any text it will contain the text 'filter list' so this method
     * accounts for that.
     */
    var getFilterText = function () {
        return $('#users-filter').val();
    };

    /**
     * Applies the filter found in the filter expression text field.
     */
    var applyFilter = function () {
        // get the dataview
        var usersGrid = $('#users-table').data('gridInstance');

        // ensure the grid has been initialized
        if (nfCommon.isDefinedAndNotNull(usersGrid)) {
            var usersData = usersGrid.getData();

            // update the search criteria
            usersData.setFilterArgs({
                searchString: getFilterText(),
                property: $('#users-filter-type').combo('getSelectedOption').value
            });
            usersData.refresh();
        }
    };

    /**
     * Performs the filtering.
     *
     * @param {object} item     The item subject to filtering
     * @param {object} args     Filter arguments
     * @returns {Boolean}       Whether or not to include the item
     */
    var filter = function (item, args) {
        if (args.searchString === '') {
            return true;
        }

        try {
            // perform the row filtering
            var filterExp = new RegExp(args.searchString, 'i');
        } catch (e) {
            // invalid regex
            return false;
        }

        return item.component[args.property].search(filterExp) >= 0;
    };

    /**
     * Builds a listing a users.
     */
    var buildUsersList = function () {
        var usersGrid = $('#users-table').data('gridInstance');
        var usersData = usersGrid.getData();
        var usersList = $('#available-users');

        // add a row for each user
        var count = 0;
        $.each(usersData.getItems(), function (_, user) {
            if (user.type === 'user') {
                // checkbox
                var checkbox = $('<div class="user-check nf-checkbox checkbox-unchecked"></div>').addClass('group-user-' + user.id);

                // user id
                var userId = $('<span class="user-id hidden"></span>').text(user.id);

                // identity
                var identity = $('<div class="available-identities nf-checkbox-label"></div>').text(user.component.identity);

                // clear
                var clear = $('<div class="clear"></div>');

                // list item
                var li = $('<li></li>').append(checkbox).append(userId).append(identity).append(clear).appendTo(usersList);
                if (count++ % 2 === 0) {
                    li.addClass('even');
                }
            }
        });
    };

    /**
     * Builds a listing a users.
     */
    var buildGroupsList = function () {
        var usersGrid = $('#users-table').data('gridInstance');
        var usersData = usersGrid.getData();
        var groupsList = $('#available-groups');

        // add a row for each user
        var count = 0;
        $.each(usersData.getItems(), function (_, group) {
            if (group.type === 'group') {
                // checkbox
                var checkbox = $('<div class="group-check nf-checkbox checkbox-unchecked"></div>').addClass('user-group-' + group.id);

                // group id
                var groupId = $('<span class="group-id hidden"></span>').text(group.id);

                // icon
                var groupIcon = $('<div class="fa fa-users nf-checkbox-label"></div>');

                // identity
                var identity = $('<div class="available-identities nf-checkbox-label"></div>').text(group.component.identity);

                // clear
                var clear = $('<div class="clear"></div>');

                // list item
                var li = $('<li></li>').append(checkbox).append(groupId).append(groupIcon).append(identity).append(clear).appendTo(groupsList);
                if (count++ % 2 === 0) {
                    li.addClass('even');
                }
            }
        });
    };

    /**
     * Edit's the specified user's account.
     *
     * @argument {object} user        The user item
     */
    var editUser = function (user) {
        // populate the users info
        $('#user-id-edit-dialog').text(user.id);
        $('#user-identity-edit-dialog').val(user.component.identity);

        if (user.type === 'user') {
            $('#individual-radio-button').prop('checked', true);
            $('#user-groups').show();
            $('#group-members').hide();

            // list all the groups currently in the table
            buildGroupsList();

            // select each group this user belongs to
            $.each(user.component.userGroups, function (_, userGroup) {
                $('div.group-check.user-group-' + userGroup.id).removeClass('checkbox-unchecked').addClass('checkbox-checked');
            });
        } else {
            $('#group-radio-button').prop('checked', true);
            $('#user-groups').hide();
            $('#group-members').show();

            // list all the users currently in the table
            buildUsersList();

            // select each user that belongs to this group
            $.each(user.component.users, function (_, member) {
                $('div.user-check.group-user-' + member.id).removeClass('checkbox-unchecked').addClass('checkbox-checked');
            });
        }

        // disable the type radio
        $('#user-dialog input[name="userOrGroup"]').attr('disabled', true);

        // show the dialog
        $('#user-dialog').modal('show');
    };

    /**
     * Delete's the specified user's account.
     *
     * @argument {object} user        The user item
     */
    var deleteUser = function (user) {
        // populate the users info
        // extended id must be used to get the proper entity in initUserDeleteDialog.handler
        $('#user-extended-id-delete-dialog').val(user.extendedId);
        $('#user-identity-delete-dialog').text(user.component.identity);

        // show the dialog
        $('#user-delete-dialog').modal('show');
    };

    /**
     * Open's a view of the specified user's policies.
     *
     * @argument {object} user        The user item
     */
    var viewUserPolicies = function (user) {
        var userPoliciesGrid = $('#user-policies-table').data('gridInstance');
        var userPoliciesData = userPoliciesGrid.getData();

        // begin the update
        userPoliciesData.beginUpdate();

        // set the rows
        if (nfCommon.isDefinedAndNotNull(user.component.accessPolicies)) {
            userPoliciesData.setItems(user.component.accessPolicies);
        }

        $('#policies-dialog-user-name').text(user.component.identity);

        // end the update
        userPoliciesData.endUpdate();

        // re-sort and clear selection after updating
        userPoliciesData.reSort();
        userPoliciesGrid.invalidate();
        userPoliciesGrid.getSelectionModel().setSelectedRows([]);

        // show the dialog
        $('#user-policies-dialog').modal('show');
        userPoliciesGrid.resizeCanvas();
    };

    var nfUsersTable = {
        init: function (configurableUsersAndGroups, disconnectionAcknowledged) {
            isDisconnectionAcknowledged = disconnectionAcknowledged;

            initUserDialog();
            initUserPoliciesDialog();
            initUserPoliciesTable();
            initUserDeleteDialog();
            initUsersTable(configurableUsersAndGroups);

            if (configurableUsersAndGroups) {
                $('#new-user-button').show();

                if (nfCommon.canModifyTenants()) {
                    $('#new-user-button').on('click', function () {
                        buildUsersList();
                        buildGroupsList();

                        // show the dialog
                        $('#user-dialog').modal('show');

                        // set the focus automatically, only when adding a new user
                        $('#user-identity-edit-dialog').focus();
                    });

                    $('#new-user-button').prop('disabled', false);
                } else {
                    $('#new-user-button').prop('disabled', true);
                }
            }
        },

        /**
         * Update the size of the grid based on its container's current size.
         */
        resetTableSize: function () {
            var usersTable = $('#users-table');
            if (usersTable.is(':visible')) {
                var grid = usersTable.data('gridInstance');
                if (nfCommon.isDefinedAndNotNull(grid)) {
                    grid.resizeCanvas();
                }
            }
        },

        /**
         * Load the users table.
         */
        loadUsersTable: function () {
            var users = $.ajax({
                type: 'GET',
                url: config.urls.users,
                dataType: 'json'
            });

            var groups = $.ajax({
                type: 'GET',
                url: config.urls.userGroups,
                dataType: 'json'
            });

            return $.when(users, groups).done(function (usersResults, groupsResults) {
                var usersResponse = usersResults[0];
                var groupsResponse = groupsResults[0];

                // update the refresh timestamp
                $('#users-last-refreshed').text(usersResponse.generated);

                var usersGrid = $('#users-table').data('gridInstance');
                var usersData = usersGrid.getData();

                // begin the update
                usersData.beginUpdate();

                var users = [];

                // add each user
                $.each(usersResponse.users, function (_, user) {
                    // id must be extended with type to avoid conflict with groups in some cases
                    users.push($.extend({
                        extendedId: 'user_' + user.id,
                        type: 'user'
                    }, user));
                });

                // add each group
                $.each(groupsResponse.userGroups, function (_, group) {
                    // id must be extended with type to avoid conflict with users in some cases
                    users.push($.extend({
                        extendedId: 'group_' + group.id,
                        type: 'group'
                    }, group));
                });

                // set the rows
                usersData.setItems(users, 'extendedId');

                // end the update
                usersData.endUpdate();

                // re-sort and clear selection after updating
                usersData.reSort();
                usersGrid.invalidate();
                usersGrid.getSelectionModel().setSelectedRows([]);

                $('#total-users').text(usersData.getLength());
            }).fail(nfErrorHandler.handleAjaxError);
        }
    };

    return nfUsersTable;
}));
