// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.
/*global clearTimeout*/
/**
 * @type {MasterTableView}
 */
define(['hp/core/Localizer',
    'hp/core/Style',
    'jquery',
    'lib/jquery.dataTables'],
function (localizer, style) {
"use strict";

    var MasterTableView = (function () {

        var MASTER_PANE = '.hp-master-pane';
        var TABLE = '.hp-master-table';
        var SCROLLER = '.dataTables_scrollBody';
        var TABLE_WRAPPER = '.dataTables_wrapper';
        var HEADER_PARTS = '.dataTables_scrollHead, .dataTables_scrollHeadInner, ' +
            '.dataTables_scrollHeadInner > table';
        var SORT_HEADER_CELLS =
                '.dataTables_scrollHead thead td.sorting_disabled';
        var TABLE_BODY = '> tbody';
        var ROW = TABLE_BODY + ' > tr';
        var FIRST_ROW = ROW + ':first-child';
        var ROW_COLLAPSER = '> td > .hp-collapser';
        var SCROLL_ROW = ROW + '.hp-scroll-first';
        var SELECTED = 'hp-selected';
        var ACTIVE = 'hp-active';
        var EXPANDED = 'hp-expanded';
        var SCROLL_FIRST = 'hp-scroll-first';
        var EXPAND_ROW_ON_CLICK = false;
        var KEY_DOWN = 40;
        var KEY_UP = 38;
        var TABLE_NO_CHANGE = "TABLE_NO_CHANGE";
        var TABLE_NEEDS_RELOAD = "TABLE_NEEDS_RELOAD";
        var TABLE_CHANGED = "TABLE_CHANGED";
        var LOADMORE_SPINNER = '<div class="hp-spinner-small"><div class="hp-spinner-image"></div></div>';
        var LOADMORE = 'loadMore';
        var LOADMORE_ABOVE = 'loadMoreAbove';
        var REFRESH = 'refresh';

        /**
         * @constructor
         */
        function MasterTableView() {

            var presenter = null;
            var page = null;
            var container = null;
            var pane = null;
            var table = null;
            var dataTable = null;
            var scroller = null;
            var sortColumnNames = [];
            var resource = null;
            var detailsRenderer = null;
            var detailsCollapsed = null;
            var manualScrollTo = -1;
            // scrollRowOffset tracks the offset of the first visible row w.r.t.
            // the top of the table rows.
            // It is -2 at first, and -1 when index results are processed.
            var scrollRowOffset = -2;
            var scrollRowTopDelta = 0;
            var manageWidth = false;
            var layoutTimer = null;
            var relayoutTimer = null;
            var addHelp = '';
            var addActionLink = '';
            var fnClearRowCallback;
            // remember how many rows is in the table before "Load more" is clicked;
            // it is used to maintain the scroll position to the previous Top
            var prevLayoutSpec = {
                    targetTableWidth: 0,
                    targetPaneWidth: 0,
                    tableWidth: 0,
                    paneWidth:0
                };

            function fireRelayout() {
                // we delay so any CSS animation can finish
                clearTimeout(relayoutTimer);
                relayoutTimer = setTimeout(function () {
                    page.trigger('relayout');
                }, style.animationDelay());
            }

            function expandRow(row, details) {
                if (! $(ROW_COLLAPSER, row).hasClass(ACTIVE)) {
                    $(ROW_COLLAPSER, row).addClass(ACTIVE);
                    row.addClass(EXPANDED);
                    if (! details) {
                        details = detailsRenderer(dataTable.fnGetData(row[0]));
                        details.show();
                    }
                    var detailsRow = dataTable.fnOpen(row[0], details[0],
                        'hp-row-details-cell');
                    $(detailsRow).addClass('hp-row-details-row');
                    if (row.hasClass(SELECTED)) {
                        $(detailsRow).addClass(SELECTED);
                    }
                }
            }

            function collapseRow(row) {
                if ($(ROW_COLLAPSER, row).hasClass(ACTIVE)) {
                    $(ROW_COLLAPSER, row).removeClass(ACTIVE);
                    if (detailsCollapsed) {
                        detailsCollapsed(dataTable.fnGetData(row[0]));
                    }
                    row.removeClass(EXPANDED);
                    dataTable.fnClose(row[0]);
                }
            }

            function toggleExpansion(row) {
                if (row.hasClass(EXPANDED)) {
                    collapseRow(row);
                } else {
                    expandRow(row);
                }
            }
            
            function getRowByUri(uri) {
                var tableNodes = dataTable.fnGetNodes();
                var row, indexResult;
                for (var i = tableNodes.length; i--;) {
                    row = tableNodes[i];
                    indexResult = dataTable.fnGetData(row);
                    if (indexResult && indexResult.uri === uri) {
                        row = $(row);
                        break;
                    }
                    row = undefined;
                }
                return row;
            }

            // remove all selection classes
            function clearTableSelection() {
                $(dataTable.fnSettings().aoData).each(function (index, item) {
                    $(item.nTr).removeClass(SELECTED);
                });
            }

            function setTableSelection(uris) {
                if (uris && uris.length > 0) {
                    var rows = $(ROW, table);
                    $.each(rows, function (index, row) {
                        var indexResult = dataTable.fnGetData(row);

                        if ((indexResult) &&
                            ($.inArray(indexResult.uri, uris) !== -1)) {
                            $(row).addClass(SELECTED);
                            if ($(row).hasClass(EXPANDED)) {
                                $(row).next().addClass(SELECTED);
                            }
                        } else {
                            if ($(row).hasClass(EXPANDED)) {
                                $(row).next().removeClass(SELECTED);
                            }
                        }
                    });
                }
            }

            function resetSelection(selection) {
                clearTableSelection();
                setTableSelection(selection.uris);
            }

            // Moved out of onRowClick for sonar
            function handleRowClickSelection(row, event) {
                // ignore if already selected and clicking collapser
                if (!(row.hasClass(SELECTED) &&
                      $(event.target).hasClass('hp-collapser'))) {
                    var indexResult = dataTable.fnGetData(row[0]);
                    presenter.select(indexResult,
                        (event.ctrlKey || event.metaKey), event.shiftKey);
                    if (event.ctrlKey || event.metaKey || event.shiftKey) {
                        document.getSelection().removeAllRanges();
                    }
                }
            }

            /**
             * @private
             * Called when a row is clicked on in the table
             * @param {Event} event The javascript click event
             */
            function onRowClick(event) {
                // get index result from object and call presenter
                var row = $(event.currentTarget);
                if (! row.hasClass('hp-row-details-row')) {
                    if (EXPAND_ROW_ON_CLICK && detailsRenderer) {
                        toggleExpansion(row);
                    }

                    handleRowClickSelection(row, event);
                }

                // Allow for embedding input elements in row details (Activity notes)
                if (! scroller.is(':focus') &&
                    ! $(event.target).is(':focus')) {
                    scroller.focus();
                }
            }
            
            function loadMore(tableCell, fromAbove) {
                tableCell.empty().append(LOADMORE_SPINNER);
                tableCell.attr('align', 'center');
                $('.hp-spinner-small', tableCell).show();
                presenter.loadMore(fromAbove);
            }

            function onLoadMoreAboveClick(ev) {
                loadMore($(ev.target).parent(), true);
            }
            
            function onLoadMoreClick(ev) {
                loadMore($(ev.target).parent(), false);
            }
            
            function onTopClick(ev) {
                presenter.select(null);
                scroller.scrollTop(0);
                manualScrollTo = -1;
            }
            
            function isEmptyMessageAvailable() {
                return ((addHelp && addHelp.length > 0) ||
                    (addActionLink && addActionLink.length > 0));
            }
            
            function getScrollRowOffset(row) {
                var scrollRow = row || $(SCROLL_ROW, table);
                var result = 0;
                if (scrollRow.length > 0) {
                    result = scrollRow.offset().top - table.offset().top;
                }
                return result;
            }
            
            function setScrollReference() {
                if (scrollRowOffset >= 0) {
                    scrollRowOffset = getScrollRowOffset();
                    scrollRowTopDelta = scrollRowOffset - scroller.scrollTop();
                }
            }
            
            function updateScrollPosition() {
                if (-1 === scrollRowOffset) {
                    // We reset scrollRowOffset to -1 when we process an index results
                    // change. When the eventual layout() is done, we will call
                    // this function which will use the current hp-scroll-row
                    // to determine the offset.
                    scrollRowOffset = getScrollRowOffset();
                }
                if (scrollRowOffset >= 0) {
                    var newTop = (scrollRowOffset - scrollRowTopDelta);
                    scroller.scrollTop(newTop);
                }
            }
            
            function firstScrolledRow() {
                var rows = $(ROW, table);
                var length = rows.length;
                var scrollerTop = scroller.offset().top;
                var rowTop;
                var row;
                
                for (var i=0; i<length; i++) {
                    row = $(rows[i]);
                    rowTop = row.offset().top;
                    if (rowTop >= scrollerTop) {
                        break;
                    }
                }
                return row;
            }
            
            /**
             * Returns the URI of the current scroll row.
             * If the scroll row has no data, return 'top'
             */
            function rememberScroll(loadMoreAbove) {
                var scrollRow = $(SCROLL_ROW, table);
                var scrollRowUri;
                var indexResult;
                
                if (scrollRow.length > 0) {
                    indexResult = dataTable.fnGetData(scrollRow[0]);
                    if (indexResult) {
                        scrollRowUri = indexResult.uri;
                    } else if (loadMoreAbove) {
                        // user is loading more above
                        scrollRowUri = dataTable.fnGetData(scrollRow.next()[0]).uri;
                    } else {
                        scrollRowUri = 'top';
                    }
                }
                
                // don't react to scrolling while reloading
                scrollRowOffset = -1;
                
                return scrollRowUri;
            }
            
            /**
             * Restore the scroll position to the URI remembered via rememberScroll().
             */
            function restoreScroll(scrollRowUri) {
                var row, previousRow, offset;
                
                if (! scrollRowUri) {
                    // no scroll reference, scroll to first selected item
                    row = $(ROW + '.' + SELECTED, table).first();
                    if (row.length > 0) {
                        // add a padding row above if available
                        previousRow = row.prev();
                        if (previousRow.length > 0 &&
                            ! previousRow.hasClass('hp-master-table-control')) {
                            row = previousRow;
                        }
                    }
                } else if ('top' === scrollRowUri) {
                    row = $(FIRST_ROW, table);
                } else {
                    row = getRowByUri(scrollRowUri);
                }
            
                if (row && row.length > 0) {
                    $(SCROLL_ROW, table).removeClass(SCROLL_FIRST);
                    row.addClass(SCROLL_FIRST);
                    // we will scroll as part of the eventual layout()
                }
            }

            /**
             * Adds the "Load more" or "Add ..." row at the end.
             */
            function addControls(isCustomFilter) {
                var helpText = "";
                var actionLink = "";
                var tableEmptyMessage = "";

                if (presenter.haveMoreAbove() && $('.hp-master-load-more-above', table).length === 0) {
                    // we add data-uri so scroll preservation has something to anchor on
                    $(TABLE_BODY, table).prepend(
                        '<tr class="hp-master-table-control">' +
                        '<td colspan="10">' +
                        '<a class="hp-master-load-more-above">' +
                        localizer.getString('core.master.showMore') + 
                        '</a>' +
                        '<a class="hp-master-load-top">' +
                        localizer.getString('core.master.top') + 
                        '</a>' +
                        '</td></tr>');
                    $('.hp-master-load-more-above', table).on('click', onLoadMoreAboveClick);
                    $('.hp-master-load-top', table).on('click', onTopClick);
                }
                
                if (presenter.haveMore() && $('.hp-master-load-more', table).length === 0) {
                    // add "Load more" row
                    $(TABLE_BODY, table).append(
                        '<tr class="hp-master-table-control">' +
                        '<td colspan="10">' +
                        '<a class="hp-master-load-more">' +
                        localizer.getString('core.master.showMore') +
                        '</a>' +
                        '</td></tr>');
                    $('.hp-master-load-more', table).on('click', onLoadMoreClick);
                } else if (! presenter.haveSome() && 
                        isEmptyMessageAvailable() && 
                        ! isCustomFilter) {
                    if (addHelp && addHelp.length > 0) {
                        helpText = $('<div></div>').addClass('hp-add-help').html(addHelp);
                    }
                    if (addActionLink && addActionLink.length > 0) {
                        actionLink = $('<div></div>').addClass('hp-add-action-link').html(addActionLink);
                    }
                    tableEmptyMessage = $('<div></div>').addClass('hp-help');
                    tableEmptyMessage.append(helpText);
                    tableEmptyMessage.append(actionLink);
                    var row = $('<tr class="hp-master-table-control"><td colspan="10"></td></tr>');
                    $('td', row).append(tableEmptyMessage);
                    $(TABLE_BODY, table).append(row);
                }
            }

            function removeControls() {
                $('.hp-master-load-more', table).off('click', onLoadMoreClick);
                $('.hp-master-load-more-above', table).off('click', onLoadMoreAboveClick);
                $('.hp-master-load-top', table).off('click', onTopClick);
                $('.hp-master-table-control', table).remove();
            }

            function calcTargetHeight() {
                var result;
                // use all height available in the pane that isn't used by other
                // elements in the pane
                result = pane.height();
                // don't count table headers
                result -= $('thead', table).outerHeight(true);
                result -= $('.dataTables_scrollHead', pane).outerHeight(true);
                $.each(pane.children(), function (index, child) {
                    if ($(child).is(':visible') &&
                        ! $(child).hasClass('hp-master-table') &&
                        ! $(child).hasClass('dataTables_wrapper') &&
                        ! $(child).hasClass('ui-resizable-handle')) {
                        result -= $(child).outerHeight(true);
                    }
                });
                return result;
            }
            
            function calculateRequestedTableWidth(oSettings) {
                // use any widths provided by the caller
                var result = 0, increment = 30;
                $.each(oSettings.aoColumns, function (index, column) {
                    var elem;
                    var minColWidth = $('.dataTables_scrollHead td:nth-child(' +
                            (index + 1) + ')').outerWidth(true);
                    if (column.sWidth) {
                        // use the larger of the two: sWidth, header's width
                        result += Math.max(parseInt(column.sWidth, 10), minColWidth);
                    } else {
                        elem = $('.dataTables_scrollHead td:nth-child(' +
                            (index + 1) + ')').first();
                        if ($('.hp-status', elem).length > 0) {
                            column.sWidth = '20px';
                            result += 20; // for in-progress spinner
                        } else {
                            result += $(elem).outerWidth(true);
                        }
                    }
                });
                // never go below 150, and jumps in increments of 30
                result = Math.max(150, Math.ceil(result / increment) * increment);
                return result;
            }
            
            function calculateRequestedHeaderWidth() {
                var result = 0;
                $.each($('.hp-master-header h1', pane).children(), function (index, child) {
                    if ($(child).is(':visible')) {
                       result = Math.max($(child).outerWidth(true), result);
                    }
                });
                return result;
            }
            
            function setWidths(paneWidth, tableWidth, oSettings) {
                var panePadding;
                if (manageWidth) {
                    // precalculate padding to avoid animation effects
                    panePadding = pane.outerWidth() - pane.width();
                    pane.css('width', paneWidth);
                    // master pane width is content-box
                    $('.hp-details-pane', page).css('left', paneWidth + panePadding);
                    oSettings.oScroll.sX = paneWidth;
                } else {
                    if (! pane.hasClass('hp-resized')) {
                        pane.css({'width': ''});
                    }
                }
                
                if (page) {
                    scroller.css({'width': paneWidth});
                    $(HEADER_PARTS, container).css('width', tableWidth);
                    table.css('width', tableWidth);
                } else {
                    table.css('width', '');
                }
                
                dataTable.fnAdjustColumnSizing(false);
            }
            
            function isDifferent(newLayoutSpec) {
                return ( 
                        (prevLayoutSpec.targetTableWidth !== newLayoutSpec.targetTableWidth) ||
                        (prevLayoutSpec.targetPaneWidth !== newLayoutSpec.targetPaneWidth) ||
                        (prevLayoutSpec.tableWidth !== newLayoutSpec.tableWidth) ||
                        (prevLayoutSpec.paneWidth !== newLayoutSpec.paneWidth)
                    );
            }

            function layout() {
                var oSettings = dataTable.fnSettings();
                var targetHeight = calcTargetHeight();
                var currentHeight = parseInt(scroller.css('height'), 10);
                var targetPaneWidth, targetTableWidth, targetHeaderWidth;
                var newLayoutSpec = {};
                
                manageWidth = manageWidth && ! pane.hasClass('hp-resized');
                
                if (manageWidth) {
                    // table drives pane
                    targetTableWidth = calculateRequestedTableWidth(oSettings);
                    targetHeaderWidth = calculateRequestedHeaderWidth();
                    targetPaneWidth = targetTableWidth;
                    if (table.height() >= targetHeight) {
                        targetPaneWidth = Math.max(targetHeaderWidth,
                            targetTableWidth + style.scrollBarWidth());
                        if (targetPaneWidth === targetPaneWidth) {
                            targetTableWidth = targetPaneWidth - style.scrollBarWidth();
                        }
                    } else {
                        targetPaneWidth = Math.max(targetHeaderWidth, targetTableWidth);
                        targetTableWidth = targetPaneWidth;
                    }
                } else {
                    // pane drives table
                    if (pane) {
                        targetPaneWidth = pane.width();
                        targetTableWidth = targetPaneWidth;
                        if (table.height() >= targetHeight) {
                            targetTableWidth -= style.scrollBarWidth();
                        }
                    }
                }
                
                //console.log('!!! MTV layout', table.width(),'x',currentHeight,' -> ', targetTableWidth,'x',targetHeight, ' ', pane.width(),'x',targetHeight,' -> ',targetPaneWidth,'x',targetHeight,manageWidth);
                
                if (targetHeight && targetHeight !== currentHeight) {
                    // adjust table scroll
                    oSettings.oScroll.sY = targetHeight;
                    scroller.css({'height': targetHeight});
                }

                // pane.width() returns 5 decimal places in Firefox.
                // allow some room for comparing target widths and actual widths
                if (targetTableWidth && 
                    (Math.abs(targetTableWidth - Math.floor(table.width())) > 2 || 
                    Math.abs(targetPaneWidth - Math.floor(pane.width())) > 1 )) {
                    setWidths(targetPaneWidth, targetTableWidth, oSettings);

                    // remove horizontal scroll if necessary, check if this table is in master pane
                    if ((manageWidth || (pane.hasClass('hp-master-pane') || 
                        pane.parents('.hp-master-pane').length === 1)) && table.height() >= targetHeight) {
                        pane.css('width', Math.max(table.width() +
                            style.scrollBarWidth(), pane.width()));
                    }

                    dataTable.fnDraw(false);

                    // fnDraw removes the controls
                    addControls(presenter.hasCustomFilter());

                    newLayoutSpec = {
                        targetTableWidth: targetTableWidth,
                        targetPaneWidth: targetPaneWidth,
                        tableWidth: table.width(),
                        paneWidth: pane.width()
                    };
                    
                    // Avoid calling fireRelayout if the previous calculation
                    // has not changed.
                    if (manageWidth && isDifferent(newLayoutSpec)) {
                        prevLayoutSpec = newLayoutSpec;
                        fireRelayout();
                    }
                }
                
                updateScrollPosition();
            }

            /*
             * Recursively walks the DOM removing children "bottom up"
             *   See http://msdn.microsoft.com/en-us/library/bb250448%28v=vs.85%29.aspx
             *   Specifically: DOM Insertion Order Leak Model
             *
             * We use jQuery's remove() function which is supposed to remove all eventhandlers
             *   registered with jQuery, as well as the DOM element itself.
             *
             * We are recursively removing all children under the <tbody></tbody> element.
             *   We do this from the last child to the first, each child also has its DOM tree walked
             *   in the same manner.  In this way we force clearing of the DOM from the bottom up
             *   as described in the MSDN article.
             */
            function recursiveDomRemoval(domElement) {
                var elementChildren = domElement.children;
                if (elementChildren && elementChildren.length > 0) {
                    for (var i = elementChildren.length; i--;) {
                        recursiveDomRemoval(elementChildren[i]);
                    }
                }
                $(domElement).remove();
            }

            /*
             * Every refresh we need to take care to remove the entire table DOM as well as any
             *   event handlers that were registered
             */
            function clearTableComponents() {
                /* Use the dataTable jQuery object to get all nodes currently associated with the table
                 *   Cache that value in a local variable so IE8 does not have trouble iterating over
                 *   The array.
                 *
                 * Take a look at: http://benhollis.net/experiments/browserdemos/foreach/array-iteration.html
                 *   with an IE8 browser to see why we don't use $.each or another iteration model.
                 */
                var tableNodes = dataTable.fnGetNodes();
                for (var i = tableNodes.length; i--;) {
                    var nRow = tableNodes[i];

                    //If this function is defined, an implementor has chosen to process a function for each row removal
                    if (fnClearRowCallback) {
                        fnClearRowCallback.call(nRow, nRow);  // "this" is nRow in fnClearRowCallback()
                    }

                    recursiveDomRemoval(nRow);
                }

                removeControls();

                //The last stage of DOM removal is to remove the <tbody> section
                var dataTableTBody = dataTable.children('tbody');
                if (dataTableTBody && dataTableTBody[0]) {
                    $(dataTableTBody[0]).remove();
                }

                //Now re-add the <tbody> child and set the reference so dataTable can use it in fnDraw
                dataTable.append("<tbody></tbody>");
                dataTable.fnSettings().nTBody = dataTable.children('tbody')[0];
            }

            function resetSort() {
                // reset sort indicator
                var sort = presenter.getSort();
                if (sort) {
                    $(SORT_HEADER_CELLS, container).removeClass('sort_asc sort_desc');
                    $(SORT_HEADER_CELLS + '[data-name="' + 
                        sort.name + '"]', container).addClass('sort_' + sort.direction);
                }
            }
            
            function refreshTable(indexResults, expandedUris) {
                var start = 0;
                var length = indexResults.count;
                var tableDataLength = dataTable.fnGetData().length;
                var expansionsEncountered = 0;
                var tableDataIndex, indexResult, tableData, expandedRow;
                var result = TABLE_NO_CHANGE;
                
                // replace or add rows
                for (var i=0; i<length; i++) {
                    tableDataIndex = start + i;
                    indexResult = indexResults.members[i];
                    tableData = dataTable.fnGetData(tableDataIndex);
                    
                    if (! tableData) {
                        dataTable.fnAddData(indexResult);
                        result = TABLE_CHANGED;
                    } else if (indexResult.uri !== tableData.uri) {
                        // if tasks UIRs don't match, discard and reload table
                        result = TABLE_NEEDS_RELOAD; 
                        break;
                    } else {
                        // Commenting this line, since "modified" is the new arguement which is not handled and leads an issue in updating the status in master pane
                         /*if ((indexResult.modified !== tableData.modified) ||
                                   ( indexResult.__updatedTimestamp) ) { */
                            // either the data has changed or a result has been added
                            // or removed such that the uris don't line up
                            // or we have a running task that we might need to update 
                            dataTable.fnUpdate(indexResult, tableDataIndex, undefined, false);
                            result = TABLE_CHANGED;
                        //}
                        expandedRow = dataTable.fnGetNodes(tableDataIndex);
                        if ($.inArray(indexResult.uri, expandedUris) !== -1) {
                            $(ROW_COLLAPSER, expandedRow).addClass(ACTIVE);
                        }
                    }
                }
                
                if (result !== TABLE_NEEDS_RELOAD) {
                    // remove extra rows
                    for (i=(tableDataLength-1); i>=(length + expansionsEncountered); i--) {
                        dataTable.fnDeleteRow(i, undefined, false);
                        result = TABLE_CHANGED;
                    }
                }
                
                return result;
            }

            /**
             * @private
             * Compare each entry in the dataTable with the corresponding entry in 
             * the IndexResults. 
             * @param {Object} IndexResults
             * @returns true as soon as an entry is found different, false if all entries
             * are the same
             */
            function haveDataChanged(indexResults) {
                var start = 0;
                var tableDataArray = dataTable.fnGetData();
                var tableDataLength = tableDataArray.length;
                var tableDataIndex, indexResult, tableData;
                var changed = false;
                
                for (var i=0; i<tableDataLength; i++) {
                    tableDataIndex = start + i;
                    indexResult = indexResults.members[i];
                    tableData = tableDataArray[tableDataIndex];
                    
                    if (! tableData) {
                        changed = true;
                        break;
                    } else if (indexResult.modified !== tableData.modified ||
                        indexResult.uri !== tableData.uri) {
                        changed = true;
                        break;
                    }
                }

                return changed;
            }

            /**
             * @private
             * Append new data to the end of the data table
             * @param {Object} IndexResults 
             */
            function appendToTable(indexResults) {
                var members = indexResults.members.slice();
                var idxStart = dataTable.fnGetData().length;
                var count = indexResults.count - idxStart;
                var membersToAdd = members.splice(idxStart, count);

                dataTable.fnAddData(membersToAdd);
                return membersToAdd.length > 0;
            }
            
            /**
             * @private
             * @param {Object} data the IndexResults
             */
            function onIndexResultsChange(indexResults) {
                var expandedDetails = {};
                var expandedUris;
                var scrollRowUri;
                var loadMoreAbove;
                var updateTable = TABLE_NO_CHANGE;
                
                //var startTime = new Date(), endTime;
                //console.log('!!! MTV draw', indexResults.count, indexResults.refreshed);
                
                if (indexResults) {
                    
                    $(TABLE, page).removeClass('hp-changing');
                    $(TABLE_WRAPPER, page).removeClass('hp-empty');
                    loadMoreAbove = (indexResults.__generatedBy === LOADMORE_ABOVE);
                    
                    if (0 === indexResults.total) {
                        
                        var oSettings = dataTable.fnSettings();
                        var oLanguage = $.extend(true, {}, oSettings.oLanguage);
                        oSettings.oLanguage = oLanguage;
                        oLanguage.sEmptyTable = presenter.getEmptyMessage(indexResults);
                        
                        clearTableComponents();
                        dataTable.fnClearTable(true);                        
                        
                    } else {
                    
                        // remember which rows were expanded
                        expandedUris = $(ROW + '.' + EXPANDED, table).map(
                            function (index, row) {
                                var uri = dataTable.fnGetData(row).uri;
                                expandedDetails[uri] =
                                    $('.hp-row-details-cell', $(row).next()).children();
                                return uri;
                            }).get();
                        
                        // remember where we were scrolled to
                        scrollRowUri = rememberScroll(loadMoreAbove);
                        
                        if (indexResults.__generatedBy === REFRESH) {
                            updateTable = refreshTable(indexResults, expandedUris);
                        } else {
                            if ((indexResults.__generatedBy === LOADMORE) && 
                                (!haveDataChanged(indexResults))) {
                                // This is called from loadMore. If the original set of data have not been
                                // modified, then just append the new data to the table to speed things up.
                                appendToTable(indexResults);
                            } else {
                                updateTable = TABLE_NEEDS_RELOAD;
                            }
                        }
                    
                        if (updateTable === TABLE_NEEDS_RELOAD) {  // need to reload the whole table
                            // detach expanded rows
                            var length = expandedUris.length;
                            for (var i=0; i<length; i++) {
                                expandedDetails[expandedUris[i]].detach();
                            }
                        
                            clearTableComponents();
                            dataTable.fnClearTable(false);
                            dataTable.fnAddData(indexResults.members, false);
                        }

                        // if we added/updated/deleted anything, redraw
                        if (updateTable !== TABLE_NO_CHANGE) {
                            dataTable.fnDraw(false);
                        }
                        
                        addControls(indexResults.filter.isCustom());
                        resetSort();
                        
                        if (updateTable === TABLE_NEEDS_RELOAD) {
                            
                            // have to do this after redrawing or expansion is lost
                            $(ROW, table).each(function (index, row) {
                                var indexResult = dataTable.fnGetData(row);
                                if (indexResult &&
                                    ($.inArray(indexResult.uri, expandedUris) !== -1)) {
                                    expandRow($(row), expandedDetails[indexResult.uri]);
                                }
                            });
                            
                            resetSelection(presenter.getSelection());
                        }
                        
                        restoreScroll(scrollRowUri);
                    }
                    
                    if (updateTable !== TABLE_NO_CHANGE) {
                        table.trigger('relayout');
                    }
                }
                //endTime = new Date();
                //console.log('!!! master table took', (endTime.getTime() - startTime.getTime()) + "ms", 'to do', indexResults.count);
            }

            function onIndexResultsError(errorInfo) {
                if (page) {
                    $(TABLE, page).removeClass('hp-changing');
                    $(TABLE_WRAPPER, page).addClass('hp-empty');
                }
            }
            
            function onIndexResultsChanging() {
                if (page) {
                    $(TABLE, page).addClass('hp-changing');
                }
            }

            // align table row selection classes with uris
            function onSelectionChange(selection) {
                resetSelection(selection, false);
            }

            // Invalidate current selection
            function onInvalidSelection(selection) {
                $(TABLE, page).removeClass('hp-changing');
                clearTableSelection();
            }
            
            function onScroll(event) {
                if (scrollRowOffset >= 0) {
                    // stop auto scrolling if the user scrolls
                    manualScrollTo = scroller.scrollTop();
                    $(SCROLL_ROW, table).removeClass(SCROLL_FIRST);
                    // if at top and no selection, turn off manual scroll
                    if (manualScrollTo === 0 && resource.getSelection().uris.length === 0) {
                        manualScrollTo = -1;
                    } else {
                        var row = firstScrolledRow();
                        row.addClass(SCROLL_FIRST);
                    }
                    setScrollReference();
                }
            }

            function onKeyDown(event) {
                if (scroller.is(":focus")) {
                    var keyCode = (event.which ? event.which : event.keyCode);
                    if (keyCode === KEY_DOWN) {
                        manualScrollTo = -1;
                        $('.hp-master-table tr.hp-selected').next().trigger('click');
                        event.stopPropagation();
                        return false;
                    } else if (keyCode === KEY_UP) {
                        manualScrollTo = -1;
                        $('.hp-master-table tr.hp-selected').prev().trigger('click');
                        event.stopPropagation();
                        return false;
                    }
                }
            }

            // called when the user clicks on a table column header
            function sortColumn(event) {
                var cell = $(event.currentTarget);
                var propertyName = cell.attr('data-name');
                if (cell.hasClass('sort_asc')) {
                    $(SORT_HEADER_CELLS, container).removeClass('sort_asc sort_desc');
                    cell.addClass('sort_desc');
                    presenter.setSort(propertyName, 'desc');
                } else {
                    $(SORT_HEADER_CELLS, container).removeClass('sort_asc sort_desc');
                    cell.addClass('sort_asc');
                    presenter.setSort(propertyName, 'asc');
                }
            }

            function onResize(event) {
                // IE8 assigns the onResize event to the document object not
                // the window object so testing for document as well
                if (event.target === window || event.target === document) {
                    clearTimeout(layoutTimer);
                    layoutTimer = setTimeout(layout, 50);
                } else if (event.target === page[0]) {
                    clearTimeout(layoutTimer);
                    layout();
                }
            }
            
            function onReset(event) {
                $(SCROLL_ROW, table).removeClass(SCROLL_FIRST);
            }

            /**
             * @private
             * Initialize the master table and attach event handlers from the presenter
             * @param {object} optionsArg Any options to pass to jQuery.datatables.
             */
            function masterTableInit(optionsArg) {
              
                table.addClass('hp-selectable');

                if (page && pane) {
                    manageWidth = (pane.hasClass('hp-master-pane') ||
                        pane.parents('.hp-master-pane').length === 1);
                }

                var options = {
                    "bPaginate" : false,
                    "bFilter" : false,
                    "bInfo" : false,
                    "bDeferRender": true,
                    "sScrollY" : calcTargetHeight(),
                    "sScrollX" : (page ? undefined : '100%'),
                    "bAutoWidth": false,
                    "bSort": false,
                    "aaData" : []
                };
                $.extend(options, optionsArg);

                fnClearRowCallback = optionsArg.fnClearRowCallback;
                var oLanguage = $.extend({}, localizer.getString('core.dataTables.oLanguage'));
                if (oLanguage) {
                    options.oLanguage = oLanguage;
                    oLanguage.sEmptyTable = presenter.getEmptyMessage();
                }

                // Initialize dataTable
                dataTable = table.dataTable(options);

                scroller = $(SCROLLER, container);
                scroller.attr('tabindex', '1');
                scroller.scroll(onScroll);

                var sortable = $.map(options.aoColumns, 
                    function (col) {
                        return col.hasOwnProperty('bSortable') ? col.bSortable : true;
                    });

                // We do our own sorting, since we don't give datatables all
                // the data and we use the index service to sort.
                sortColumnNames = $.map(options.aoColumns, function (col) {
                    var match;
                    if (col.mDataProp) {
                        // strip leading "attributes.", if any
                        match = col.mDataProp.match(/^(multiA|a)ttributes\.(.*)/);
                        if (match) {
                            return match[2];
                        } else {
                            return col.mDataProp;
                        }
                    } else {
                        return ''; // if undefined, so be it
                    }
                });

                // Set up the header cells for sorting, store the sort
                // property name as a data- attribute.
                $(SORT_HEADER_CELLS, container).addClass("sort").each(
                    function (index, cell) {
                        if (sortable[index]) {
                            $(cell).click(sortColumn);
                        }
                        $(cell).attr('data-name', sortColumnNames[index]);
                    });

                // do initial sorting, if any
                if (options.aaSorting && options.aaSorting.length > 0) {
                    var sortOpt = options.aaSorting[0];
                    if (sortColumnNames[sortOpt[0]]) {
                        presenter.setSort(sortColumnNames[sortOpt[0]],
                            sortOpt[1]);
                    }
                }

                table.on('click', 'tbody tr', onRowClick);

                if (detailsRenderer && ! EXPAND_ROW_ON_CLICK) {
                    table.on('click', 'tbody tr td .hp-collapser',
                        function (ev) {
                            toggleExpansion($(this).parents('tr'));
                        });
                }
            }

            /**
             * @public
             * Stop the timer polling of the index service.
             */
            this.pause = function () {
                // Atlas 2.1 Integration: Disabling onIndexResultsChanging to make master pane visible always
                //presenter.off("indexResultsChanging", onIndexResultsChanging);
                presenter.off("indexResultsChange", onIndexResultsChange);
                presenter.off("indexResultsError", onIndexResultsError);
                presenter.off("selectionChange", onSelectionChange);
                presenter.off("invalidSelection", onInvalidSelection);
                presenter.off("reset", onReset);
                $(window).off('resize', onResize);
                if (page) {
                    page.off('relayout', layout);
                }
            };

            /**
             * @public
             * Resume the timer polling of the index service.
             */
            this.resume = function () {
                resetSelection(presenter.getSelection());
                manualScrollTo = -1;
                scrollRowOffset = -2;
                // Atlas 2.1 Integration: Disabling onIndexResultsChanging to make master pane visible always
                //presenter.on("indexResultsChanging", onIndexResultsChanging);
                presenter.on("indexResultsChange", onIndexResultsChange);
                presenter.on("indexResultsError", onIndexResultsError);
                presenter.on("selectionChange", onSelectionChange);
                presenter.on("invalidSelection", onInvalidSelection);
                presenter.on("reset", onReset);
                $(window).on('resize', onResize);
                if (page) {
                    page.on('relayout', layout);
                }
                resetSort();
            };

            /**
             * @public
             * Intialize the view.
             */
            this.init = function (presenterArg, args) {
                presenter = presenterArg;
                resource = args.resource;
                if (args.page) {
                    page = args.page;
                } else {
                    page = $('.hp-page');
                }
                if (args.table) {
                    table = $(args.table);
                } else {
                    table = $(TABLE, page);
                }
                container = table.parent();
                if (args.pane) {
                    pane = $(args.pane);
                } else if ($(MASTER_PANE, page).length > 0) {
                    pane = $(MASTER_PANE, page);
                } else {
                    pane = container;
                }
                detailsRenderer = args.detailsRenderer;
                detailsCollapsed = args.detailsCollapsed;
                addHelp = args.addHelp;
                addActionLink = args.addActionLink;

                masterTableInit(args.dataTableOptions);

                scroller.on('focus', function (event) {
                    if (event.target === scroller[0]) {
                        $(document).off('keydown', onKeyDown);
                        $(document).on('keydown', onKeyDown);
                    }
                });
                scroller.on('blur', function (event) {
                    if (event.target === scroller[0]) {
                        $(document).off('keydown', onKeyDown);
                    }
                });
            };
        }

        return MasterTableView;

    }());

    return MasterTableView;
});
