// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.
/*global clearTimeout */
define(['hp/core/EventDispatcher',
    'hp/core/Router',
    'hp/core/UrlFragment',
    'hp/core/LinkTargetBuilder',
    'hp/services/IndexService',
    'hp/services/IndexFilter',
    'hp/model/DevelopmentSettings',
    'hp/model/Session',
    'hp/core/Banner',
    'hp/core/Localizer',
    'hp/services/Log',
    'hp/model/SelectionValidator'],
function(EventDispatcher, router, urlFragment, linkTargetBuilder, indexService, IndexFilter,
    settings, session, banner, localizer, log, SelectionValidator) { "use strict";

    /*
     * Manages a Resource, its index results, selection, and filter.
     *
     * Takes three types of input:
     * 1) view changes, such as selecting something or changing a filter
     * 2) browser location changes via the Router
     * 3) Resource changes, such as arrival of index results
     * The typical flow is 1-2-3: user makes a change -> update location,
     * react to location change by adjusting the Resource.
     * But the browser location can change at any time, so sometimes it's 2-1-3.
     * And, for managing the selection, especially multi-select, it can be 1-3-2,
     * because the Resource controls the multi-select id.
     *
     * We prioritize whatever the browser URL indicates.
     * If there's a resource URI that doesn't exist, we leave it there so we
     * can indicate that to the user.
     * If the user changes a filter or searches, we start by clearing any
     * selection in the browser URL. However, if, after the index results return,
     * we find that the prior selected URI matches, we restore the selection.
     * Otherwise, we select a new resource.
     *
     * Some interesting scenarios covered:
     *  #/view/r/resource ->
     *  #/view/r/non-existing-resource -> show error
     *  #/view/r/resource?filter ->
     *  #/view/r/unmatching-resource?filter -> show unmatched
     *  #/view/r/non-existing-resource?filter -> show error
     *  #/view/r/resource (empty) -> show error and empty
     *  #/view/r/unmatching-resource?filter (empty) -> show empty
     *  #/view/r/non-existing-resource?filter (empty) -> show error and empty
     */

    var MasterPanePresenter = (function() {

        var ROUTE_SUFFIX = '(/.*|$)';
        var EXIST        = 'exist';
        var AUTOSELECT   = 'autoselect';
        var SELECT_IF_EXIST = 'selectIfExist';
        var LOADMORE     = 'loadMore';
        var LOADMORE_ABOVE = 'loadMoreAbove';
        var RELOAD       = 'reload';
        var REFRESH      = 'refresh';
        var RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND";

        /**
         * @constructor
         * @type {MasterPanePresenter}
         */
        function MasterPanePresenter() {
            var dispatcher = new EventDispatcher();
            var timer = null;
            var resource = null;
            var routePrefix = null;
            var authCategory = null;
            var defaultMetaQuery = null;
            var multiSelect = false;
            var requireSelection = true;
            var location = null;
            var awaitingResults = false;
            var refreshing = false;
            // Set this flag whenever loadMore() method is called to fetch
            // more data. It is reset when the indexResults is returned.
            var loadingMore = false;
            // Set this flag whenever loadMoreAbove() method is called to fetch
            // more data. It is reset when the indexResults is returned.
            var loadingMoreAbove = false;
            // Set this flag when the location does not specify selection.
            var selectFromResults = false;
            var initialSort = null;
            var headerTitle = '';
            // Set this flag when 1 or more selected items can not be found in the
            // index service. If set, the LHS is cleared and the RHS displays the 
            // "item not found" message
            var invalidSelection = false;
            // Flag indicating this pane presenter is the global view
            var globalView = true;
            // Current SelectionValidator instance object
            var currentValidator = null;
            // Selection specified by the route
            var selectionInRoute = {uris: [], multiSelectId: null};
            var lastIndexResults = null;
            
            // Prototyping method to make sonar happy
            var getIndexResults;
            var validateSelection;

            // Find if the selection is being removed
            function isSelectionBeingRemoved(selection) {
                var beingRemoved = false;
                if (selection && selection.uris && selection.uris.length > 0) {
                    for (var i = 0; i < selection.uris.length; i++) {
                        if (resource.isBeingRemoved(selection.uris[i])) {
                            beingRemoved = true;
                            break;
                        }
                    }
                }
                return beingRemoved;
            }

            function notifySelectionInvalid(selection) {
                // fire events only if it has not already been done.
                if (!invalidSelection && !isSelectionBeingRemoved(selection)) {
                    invalidSelection = true;
                    // if multiselect, then invalidate the id                               
                    if (selection.uris.length > 1) {
                        resource.invalidateMultiSelectId(selection.multiSelectId);
                    }
                    resource.clearAllSelectedUris();
                    // clear the selection from the master*view
                    dispatcher.fire("invalidSelection", selection);
                }
            }

            function startsWith(string, pattern) {
                return (pattern === string.substr(0, pattern.length));
            }

            /**
             * @private
             * Start polling the index service for results.
             */
            function restartTimer(always) {
                var doStart = always;
                if (settings.getRefreshInterval()) {
                    if (timer) {
                        clearTimeout(timer);
                        doStart = true;
                    }
                    if (doStart) {
                        timer = setTimeout(function () {
                            refreshing = true;
                            getIndexResults();
                        }, settings.getRefreshInterval());
                    }
                }
            }

            // Method prototyped above to make sonar happy
            getIndexResults = function() {
                if (! awaitingResults) {
    //                awaitingResults = true;
                    if (! refreshing) {
                        dispatcher.fire("indexResultsChanging");
                    }
                    resource.getIndexResults({
                        error: function () {
                            awaitingResults = false;
                            restartTimer();
                        },
                        success: function () {
                            awaitingResults = false;
                            restartTimer();
                        }
                    });
                }
            };
            
            function resetGetResults() {
                clearTimeout(timer);
                refreshing = false;
                getIndexResults();
            }
            
            /**
             * @private
             * Called when the "Load more" link is clicked. 
             * @param {boolean} above True if load more from above; 
             *                        False if load more from below.
             */
            function loadMore(above) {
                var newFilter;
                newFilter = new IndexFilter(resource.getIndexFilter());
                
                if (above) {
                    newFilter.decrementStart(resource.getStart());
                }
                newFilter.bumpCount();
                
                if (resource.setIndexFilter(newFilter)) {
                    loadingMoreAbove = above;
                    loadingMore = !above;
                    resetGetResults();
                }
            }

            // Preprocess the new route to determine:
            // 1) If the route is triggered by a "reset all" filter clicked. If so,
            //    the selections are removed from the location and reroute.
            // 2) If the old sort should be preserved.
            // 3) If the old query should be preserved. If so, then the old
            //    filter is installed, and then reroute.
            function preprocessRoute(newFilter, location, noSelectionInRoute) {
                var oldFilter = resource.getIndexFilter();
                var rerouted = false;

                // if the location has a sort, keep it
                if (! newFilter.getSort()) {
                    // if we have a sort already, use it
                    if (oldFilter && oldFilter.getSort()) {
                        newFilter.setSort(oldFilter.getSort());
                    } else if (initialSort) {
                        newFilter.setSort(initialSort);
                    }
                }

                // if we have paged, keep the pages
                newFilter.useCount(oldFilter);
                
                // Keep track of where we started
                newFilter.useStart(oldFilter);

                // Determine if this route change comes from the user
                // selecting one of the "reset all" filters. If so, remove
                // the FILTER_RESET flag and the uris in the location and re-route.
                if (newFilter.hasResetFlag()) {
                    newFilter.unsetResetFlag();
                    location = newFilter.updateLocation(urlFragment.replaceUris(location, []));
                    resource.setIndexFilter(newFilter);
                    router.replaceWith(location, "master filter reset");
                    rerouted = true;
                } else if (! newFilter.getUserQuery() && noSelectionInRoute &&
                    oldFilter && oldFilter.getUserQuery()) {
                    // Determine if we need to preserve old user query. We preserve the old
                    // query if the route contains no selection, and the route
                    // does not specify query.
                    newFilter.setUserQuery(oldFilter.getUserQuery());
                    location = newFilter.updateLocation(location);
                    router.replaceWith(location, "master use old query");
                    rerouted = true;
                }

                return rerouted;
            }

            function setDocumentTitle() {
                var context = '',
                    view = urlFragment.getView(location),
                    friendlyView,
                    uris = resource.getSelection().uris,
                    indexResult;
                if (! startsWith(view, 'show')) {
                    friendlyView = view.replace(/\//g, '//').replace(/[^\w\-]./, ' ');
                    context = friendlyView.charAt(0).toUpperCase() +
                        friendlyView.slice(1) + ' ';
                }
                if (uris && uris.length > 0) {
                    if (uris.length > 1) {
                        context += uris.length;
                    } else {
                        indexResult = resource.getIndexResultForUri(uris[0]);
                        if (indexResult && indexResult.name) {
                            context += indexResult.name;
                        }
                    }
                }
                banner.setDocumentTitle(context);
            }

            // Create a new SelectionValidator instance. If an old instance
            // already exists and it is not current, cancel its operations.
            // @param {selectionToValidate} The selection the new validator operates on.
            function resetValidator(selectionToValidate) {
                var filter = new IndexFilter(resource.getIndexFilter());
                var selection = selectionToValidate ? selectionToValidate : resource.getSelection();
                
                filter.setReferenceUri(null); // Disable the use of referenceUri for validation
                // resource may not return anything because the selection can not be found and
                // therefore cleared. In that case, if the current location specifies anything
                // uris, then validate that set.
                if (selection.uris.length === 0) {
                    selection = selectionInRoute;
                }
                
                if (currentValidator && !currentValidator.isCurrent(selection, filter)) {
                    currentValidator.cancel();
                }
                
                currentValidator = new SelectionValidator();
                currentValidator.init({filter:filter, selection:selection, resource:resource});
            }

            // The success/error handlers to process the validation result for
            // the current selection. The validation can succeed before and fail later
            // when one or more items in the selection is removed from another browser.
            var existenceHandlers = {
                    success: function (validator) {
                        // if the validation failed before, reset it
                        if (invalidSelection) {
                            // hide error
                            dispatcher.fire('validSelection');
                            // reselect lhs
                            resource.setSelectedUris(validator.getSelection().uris);
                            // notify rhs
                            resource.renotify();
                            invalidSelection = false;
                        }
                    },
                    error: function (validator) {
                        notifySelectionInvalid(validator.getSelection());
                    }
                };

            // The success/error handlers to process the validation result for
            // auto-selection. Auto-select is performed when the location does not
            // specify any uris. This occurs in filter change and inter-categorical
            // navigation. The prior selection is validated. If validation fails, then
            // the first item is selected.
            var autoSelectHandlers = {
                    success: function (validator) {
                        router.replaceWith(urlFragment.replaceUris(location, validator.getSelection().uris,
                            validator.getSelection().multiSelectId), 'master pane auto select uris');
                    },
                    error: function (validator) {
                        router.replaceWith(urlFragment.replaceUris(location, [resource.getBestSelectionUri()],
                            null), 'master pane auto select first item');
                    }
                };
            
            // The success/error handlers to process the validation result for the
            // current selection. If the selection is valid, then select it. Otherwise
            // just remove the selection from the resource. These handlers are used 
            // for pages that do not require a selection (e.g. Activity).
            var selectIfExistHandlers = {
                    success: function (validator) {
                        router.replaceWith(urlFragment.replaceUris(location, validator.getSelection().uris,
                            validator.getSelection().multiSelectId), 'master pane select uris if exists');
                    },
                    error: function (validator) {
                        // if it does not exist in the result, clear all selection
                        resource.clearAllSelectedUris();
                    }
                };

            // Call index service to validate the current selection set.
            // If one or more items does not exist, the whole selection
            // set becomes invalid. 
            validateSelection = function (purpose, selection) {
                resetValidator(selection);
                switch (purpose) {
                    case EXIST:
                        currentValidator.validate(existenceHandlers);
                        break;
                    case AUTOSELECT:
                        currentValidator.validate(autoSelectHandlers);
                        break;
                    case SELECT_IF_EXIST:
                        currentValidator.validate(selectIfExistHandlers);
                        break;
                }
            };

            // Atlas 2.1 Integration: replacing setSelectionFromResults() with the implementation
            // that was already present in HPSUM, to make the first item in master pane selected by default always when page loads
            // This method is called by the onIndexResultsChanged handler.
            /*function setSelectionFromResults(indexResults) {
                var selection;
                if (0 === indexResults.total) {
                    resource.clearSelectedUris();
                } else {
                    // if we have any already selected, use them
                    selection = resource.getSelection();
                    if (selection.uris.length === 0) {
                        // attempt to restore to previous selection
                        // This can happen when the selection is cleared
                        // when we 'add' or when we change a filter but
                        // the prior selection is still available.
                        selection = resource.getPriorSelection();
                    }

                    if (requireSelection) {
                        if (selection.uris.length > 0) {
                            validateSelection(AUTOSELECT, selection);
                        } else if (indexResults.members.length > 0) {
                            router.replaceWith(urlFragment.replaceUris(location, [indexResults.members[0].uri],
                                    null), 'master pane select first uri');
                        } else {
                            router.replaceWith(urlFragment.replaceUris(location, [resource.getPostSelectedUri()],
                                    null), 'master pane select post uri');
                        }
                    } else if (selection.uris.length > 0) {
                        // Selection is not required, but preserve it if it exists
                        validateSelection(SELECT_IF_EXIST, selection);
                    }
                }
            }*/
            function setSelectionFromResults(indexResults) {
                var uris, multiSelectId, selection;
                if (0 === indexResults.total) {
                    resource.clearSelectedUris();
                } else {
                    // if we have any already selected, use them
                    selection = resource.getSelection();
                    uris = selection.uris;
                    multiSelectId = selection.multiSelectId;
                    if (uris.length === 0) {
                        // attempt to restore to previous selection
                        // This can happen when the selection is cleared
                        // when we 'add' or when we change a filter but
                        // the prior selection is still available.
                        selection = resource.getPriorSelection();
                        uris = selection.uris;
                        multiSelectId = selection.multiSelectId;
                    }

                    if (uris.length > 0) {
                        // Look for the uris in the index result.
                        var foundUris = $.grep(uris, function (uri) {
                            return resource.getIndexResultForUri(uri);
                        });

                        // If we know about these uris already, use them if
                        // 1 - all selected items are found in the loaded index result, or
                        // 2 - the user changes filter, then use whatever is found (including none)
                        if ((uris.length === foundUris.length) ||
                            (resource.getIndexFilter().getUserQuery() && !resource.haveMore())) {
                            uris = foundUris;
                        }
                    }

                    if (uris.length === 0) {
                        // we don't have any existing selection, select the first one
                        uris = [indexResults.members[0].uri];
                        multiSelectId = null;
                    }

                    router.replaceWith(urlFragment.replaceUris(location, uris,
                            multiSelectId), 'master pane select uris');
                }
            }
            // Atlas 2.1 Integration - ends

            // Return the index of the uri in indexResults
            function findIndex(indexResults, uri) {
                var index = -1;
                var length = indexResults.members.length;

                for (var i = 0; i < length; i++) {
                    if (indexResults.members[i].uri === uri) {
                        index = i;
                        break;
                    }
                }
                return index;
            }
            
            // Find the selection from the indexResults
            function findSelection(indexResults, selection) {
                var haveSelection = null,
                    beingRemoved = false,
                    uri;
                
                for(var i = 0; i < selection.uris.length; i++) {
                    uri = selection.uris[i];
                    haveSelection = (findIndex(indexResults, uri) >= 0);
                    beingRemoved = !haveSelection && resource.isBeingRemoved(uri);                   
                    if (haveSelection || beingRemoved) {
                        break;
                    }
                }
                return (haveSelection && !beingRemoved);
            }
            
            function isSameSet(indexResults) {
                var result = false;
                if (lastIndexResults &&
                    (indexResults.count === lastIndexResults.count) &&
                    (indexResults.start === lastIndexResults.start) &&
                    (indexResults.total === lastIndexResults.total) &&
                    !lastIndexResults.filter.isDifferent(indexResults.filter)) {
                    result = true;
                }
                return result;
            }
            
            // Set the result's __generatedBy property. Upstream components
            // (e.g. MasterTableView) should use this information to determine how
            // to consume the data.
            function setGeneratedBy(indexResults) {
                if (refreshing || isSameSet(indexResults)) {
                    indexResults.__generatedBy = REFRESH;
                } else if (loadingMoreAbove) {
                    indexResults.__generatedBy = LOADMORE_ABOVE;
                    loadingMoreAbove = false;
                } else if (loadingMore) {
                    indexResults.__generatedBy = LOADMORE;
                    loadingMore = false;
                } else {
                    indexResults.__generatedBy = RELOAD;
                }
            }
            
            function onIndexResultsChanged(indexResults) {
                var reloadingResults = false;
                var selection = resource.getSelection();
                var filter = resource.getIndexFilter();
                
                if (routePrefix) {
                    setDocumentTitle();
                }

                // For Atlas 2.1 and later
                // If referenceUri is used and the returned index result set is empty, 
                // then the selection is invalid
                if ((filter.hasReferenceUri()) && (indexResults.members.length === 0)) {
                    if (! resource.isBeingRemoved(filter.getReferenceUri())) {
                        notifySelectionInvalid(resource.getSelection());
                        return;
                    }
                }
                
                // Tag what this set of results is retrieved for. Upstream components need
                // to know.
                setGeneratedBy(indexResults);
                
                if (selectFromResults) {
                    setSelectionFromResults(indexResults);
                    // we might not have selected any because none exist yet.
                    if (resource.getSelection().uris.length > 0) {
                        selectFromResults = false;
                    }
                } else if (invalidSelection) {
                    // The current selection is still invalid. 
                    dispatcher.fire("invalidSelection", selection);
                } else {
                    if (selection.uris.length > 0) {
                        if (!findSelection(indexResults, selection)) {
                            // The selection can not be found in indexResults. Reload
                            // using referenceUri.
                            resource.getIndexFilter().setReferenceUri({uri: selection.uris[0]});
                            resetGetResults();
                            reloadingResults = true;
                        } else {
                            // Validate that the selected uris in current selection exist
                            // in the indexService.
                            validateSelection(EXIST);
                            resource.setPostSelection(selection.uris);
                        }
                    }
                }
                
                // Fire "indexResultsChange" event if indexResults contain selection
                if (!reloadingResults) {
                    dispatcher.fire("indexResultsChange", indexResults);
                }
                
                lastIndexResults = indexResults;
            }

            function onIndexResultsError(errorInfo) { 
                var filter = resource.getIndexFilter();
                
                if (filter.hasReferenceUri() && errorInfo.errorCode === RESOURCE_NOT_FOUND) {
                    // For Atlas 2.0
                    // REST call fails if the referenceUri is not found
                    if (! resource.isBeingRemoved(filter.getReferenceUri())) {
                        notifySelectionInvalid(resource.getSelection());
                    }
                } else {
                    dispatcher.fire("indexResultsError", errorInfo);
                }
            }

            function onSelectionChanged(selection) {
                if (routePrefix) {
                    setDocumentTitle();
                }
                dispatcher.fire("selectionChange", selection);
                if (location) {
                    if (selection.uris.length > 0) {
                        router.go(urlFragment.replaceUris(location, selection.uris,
                            selection.multiSelectId),
                            "master selection change");
                    } else if (!invalidSelection) {
                        // remove any uris from location
                        router.replaceWith(urlFragment.replaceUris(location,
                            selection.uris),
                            "master selection empty change");
                    }
                }
            }

            function getReferrer(uri) {
                indexService.getIndexForResource(uri, {
                    success: function (resources) {
                        if (resources && resources.members && resources.members[0]) {
                            dispatcher.fire("referrerChange", resources.members[0]);
                        }
                    }
                });
            }

            function onFilterChange(filter) {
                var noFilter = true;
                var referrerUri = filter.getReferrerUri();
                
                if (referrerUri) {
                    getReferrer(referrerUri);
                    headerTitle =
                        localizer.getString('core.master.header.referenced');
                    noFilter = false;
                } else {
                    dispatcher.fire("referrerChange", undefined);
                }

                if (noFilter && filter.isCustom()) {
                    headerTitle =
                        localizer.getString('core.master.header.filtered');
                    noFilter = false;
                }

                if (noFilter) {
                    headerTitle =
                        localizer.getString('core.master.header.all');
                } else if (filter.name) {
                    headerTitle = filter.name;
                }

                dispatcher.fire("filterChange", filter);
            }

            function findUrisInIndexResult(uris) {
                var foundOne = false;

                if (uris) {
                    for (var i = 0; i < uris.length; i++) {
                        foundOne = resource.getIndexResultForUri(uris[i]);
                        if (foundOne) {
                            break;
                        }
                    }
                }
                return foundOne;
            }
            
            function processRoute(uris, multiSelectId, newFilter, view) {
                var reload;
                var foundOneItem = false;
                var referenceUri = null;
                
                // If filter has changed, then the IndexResults needs to be
                // reloaded. Otherwise, the selection is made from the current
                // set of results (e.g. mouse click on item), reload is not needed. 
                reload = resource.setIndexFilter(newFilter);
                
                if (uris && uris.length === 1) {
                    // this is what the route location wants
                    resource.setSelectedUris(uris);
                    referenceUri = uris[0];
                    if (! reload) {
                        foundOneItem = findUrisInIndexResult(uris);
                    }
                } else if (multiSelectId) {
                    if (resource.selectMultiSelectId(multiSelectId)) {
                        uris = resource.getMultiSelectedUris(multiSelectId);
                        referenceUri = uris[0];
                        if (! reload) {
                            foundOneItem = findUrisInIndexResult(uris);
                        }
                    } else {
                        invalidSelection = true;
                        resource.setSelectedUris([], multiSelectId, true);
                        // clear the selection from the master*view
                        dispatcher.fire("invalidSelection", null);
                    } 
                } else {
                    // no selection in location
                    resource.clearSelectedUris();
                    if ('add' !== view.split('/')[0]) {
                        // load and select
                        selectFromResults = true;
                        reload = true;
                    }
                }
                
                if (foundOneItem) {
                    referenceUri = foundOneItem.uri;
                }

                return {reload: reload, // filter has changed, or location does not have uri
                        loadWithReferenceUri: !selectFromResults, 
                        foundOneItem: foundOneItem, 
                        referenceUri: referenceUri};
            }

            /**
             * When we show or edit, take care of managing the selection.
             * If the location has a uri, set the selection to it.
             * If it doesn't, choose the first result.
             */
            function onRouteChange(locationArg) {

                //log.log('!!! MasterPanePresenter('+routePrefix+'.onRouteChange - ' + locationArg);
                location = locationArg;
                var view = urlFragment.getView(location);
                var uris = urlFragment.getUris(location);
                var multiSelectId = parseInt(urlFragment.getMultiSelectId(location), 10);
                var noSelectionInRoute = ! (uris && uris.length > 0) && (! multiSelectId);
                var newFilter = new IndexFilter(location);
                var routeState;
                var loadOption;

                selectFromResults = false;
                selectionInRoute = {uris: uris, multiSelectId: multiSelectId};

                if (preprocessRoute(newFilter, location, noSelectionInRoute)) {
                    // Reroute has occured. Stop processing the current route.
                    return;
                }

                // If the previous selection set is invalid, reset the
                // selection states to let the current route to start fresh.
                if (invalidSelection) {
                    resource.clearAllSelectedUris();
                    invalidSelection = false;
                    dispatcher.fire("validSelection");
                }

                routeState = processRoute(uris, multiSelectId, newFilter, view);

                setDocumentTitle();

                // If there is any user query when the page is re-entered, fire  
                // "filterChage" event so that other views (e.g. SearchBoxView) sync 
                // up. This is needed in using the browser forward and back button.
                if (newFilter.getUserQuery()) {
                    dispatcher.fire("filterChange", newFilter);
                }

                if (routeState.reload) {
                    // The filter has changed. Load a new set of result and then
                    // look for the selected item.
                    resetGetResults();
                } else if (routeState.loadWithReferenceUri) {
                    loadOption = {uri: routeState.referenceUri};
                    if (routeState.foundOneItem) {
                        // update padding to maintain the selected item's relative position
                        // in subsequent indexResults retrievals
                        loadOption.padding = routeState.foundOneItem.index;
                    }
                    resource.getIndexFilter().setReferenceUri(loadOption);
                    if (!routeState.foundOneItem) {
                        resetGetResults();
                    }
                }
            }

            this.init = function(args) {
                routePrefix = args.routePrefix;
                resource = args.resource;
                authCategory = args.authCategory;
                defaultMetaQuery = args.defaultMetaQuery;
                if (args.hasOwnProperty('multiSelect')) {
                    multiSelect = args.multiSelect;
                }
                if (args.hasOwnProperty('requireSelection')) {
                    requireSelection = args.requireSelection;
                }

                headerTitle =
                    localizer.getString('core.master.header.all');

                if (routePrefix) {
                    // initialize route watching
                    router.watch(routePrefix + ' master route change',
                        '^' + routePrefix + ROUTE_SUFFIX, {change: onRouteChange});
                }

                globalView = !args.contextSensitive;
                
                if (defaultMetaQuery) {
                    var location = router.location();
                    if (! urlFragment.hasParameters(location)) {
                        var filter = new IndexFilter(location);
                        filter.setUserQuery(defaultMetaQuery);
                        router.go(filter.updateLocation(location),
                            'master default meta filter');
                    }
                }
            };

            this.getSelection = function () {
                return resource.getSelection();
            };
            
            function toggleSelection(uri) {
                var uris;
                var currentUris = resource.getSelection().uris;
                if (requireSelection &&
                    currentUris.length === 1 &&
                    currentUris[0] === uri) {
                    // can't remove last selection, no-op
                } else {
                    // remove if already there
                    uris = $.grep(currentUris, function (currentUri) {
                        return (currentUri !== uri);
                    });
                    // if we didn't remove anything, add it
                    if (uris.length === currentUris.length) {
                        uris.push(uri);
                    }
                    resource.setSelectedUris(uris);
                }
            }
            
            function setSelection(uri) {
                var currentUris;
                
                if (requireSelection) {
                    resource.setSelectedUris([uri]);
                } else {
                    currentUris = resource.getSelection().uris;
                    // if this is the only selected item, clear
                    // otherwise, set just this item
                    if (currentUris.length === 1 &&
                        currentUris[0] === uri) {
                        // get rid of the prior selection also
                        resource.clearAllSelectedUris();
                    } else {
                        resource.setSelectedUris([uri]);
                    }
                }
            }
            
            this.select = function (indexResult, toggle, contiguous) {
                var currentUris;
                
                if (indexResult) {
                    if (multiSelect) {
                        if (contiguous) {
                            resource.selectContiguousToUri(indexResult.uri);
                        } else if (toggle) {
                            toggleSelection(indexResult.uri);
                        } else {
                            setSelection(indexResult.uri);
                        }
                    } else {
                        setSelection(indexResult.uri);
                    }
                } else {
                    // if a selection is requried, we'll generate one in response,
                    // but we don't want to revert to the prior selection
                    resource.clearAllSelectedUris();
                }
            };
            
            this.loadMore = loadMore;

            this.getFilterValue = function (name) {
                var filter = resource.getIndexFilter();
                var result = 'all';
                if (filter) {
                    var value = filter.getProperty(name);
                    if (value) {
                        result = value;
                    }
                }
                return result;
            };

            this.setFilterValue = function (name, value) {
                var filter = new IndexFilter(resource.getIndexFilter());
                if (value && 'all' !== value) {
                    if (filter.setProperty(name, value)) {
                        if (globalView) {
                            // clear uris on filter change
                            router.go(urlFragment.replaceUris(
                                filter.updateLocation(location), []),
                                "master " + name + " filter change");
                        } else if (resource.setIndexFilter(filter)) {
                            //resource.getIndexResults();
                            resetGetResults();
                        }
                    }
                } else {
                    if (filter.unsetProperty(name)) {
                        if (globalView) {
                            filter.setResetFlag();
                            router.go(filter.updateLocation(location),
                                "master " + name + " filter change");
                        } else if (resource.setIndexFilter(filter)) {
                            //resource.getIndexResults();
                            resetGetResults();
                        }
                    }
                }
            };
            
            this.setMetaFilter = function (query) {
                var filter = new IndexFilter(resource.getIndexFilter());
                filter.reset();
                filter.setUserQuery(query);
                
                if (globalView) {
                    if (!query || query.trim() === '') {
                        // If there is no query or empty query string, the meta filter is to be
                        // "reset to all".  IndexFilter.setResetFlag() is needed so a call to 
                        // IndexFilter.updateLocation() will return the correct location string
                        // because at this point, the location may still contain old query string.
                        filter.setResetFlag();
                    }                    
                    router.go(filter.updateLocation(location), "master meta filter");
                } else if (resource.setIndexFilter(filter)) {
                    resetGetResults();
                }
            };

            this.resetFilters = function () {
                var filter = new IndexFilter(resource.getIndexFilter());
                if (defaultMetaQuery) {
                    filter.reset();
                    filter.setUserQuery(defaultMetaQuery);
                } else {
                    filter.setResetFlag();
                    if (globalView) {
                        filter.reset();
                    }
                }

                if (globalView) {
                    router.go(filter.updateLocation(location), "master filter reset");
                } else if (resource.setIndexFilter(filter)) {
                    resetGetResults();
                }
            };

            this.setSort = function (propertyName, direction) {
                var filter;
                if (location) {
                    filter = new IndexFilter(resource.getIndexFilter());
                    filter.setSort(propertyName, direction);
                    router.go(filter.updateLocation(location),
                        "master sort asc change");
                } else if (routePrefix) {
                    initialSort = {name: propertyName, direction: direction};
                } else {
                    filter = new IndexFilter(resource.getIndexFilter());
                    filter.setSort(propertyName, direction);
                    if (resource.setIndexFilter(filter)) {
                        //getIndexResults();
                        resetGetResults();
                    }
                }
            };

            this.getSort = function () {
                var filter = resource.getIndexFilter();
                return (filter ? filter.getSort() : null);
            };

            this.getReferrerClearLocation = function () {
                return routePrefix + '/show';
            };

            this.getSearchText = function () {
                var filter = resource.getIndexFilter();
                return (filter ? filter.getUserQuery() : '');
            };

            this.getHeaderTitle = function () {
                return headerTitle;
            };
            
            //@public
            //
            //Returns true if the add link should be shown with the empty message.
            //The link should be shown if there aren't any resources.  If we are
            //using a version of the index API which doesn't support unFilteredTotal,
            //then we can't tell if there aren't any resources when the index
            //results are filtered.  So if we are using a version of the index
            //API that doesn't support unFilteredTotal, we return false if the
            //index results are filtered.  This behavior also provides backwards
            //compatibility with how the code was working before unFilteredTotal
            //was added to the index API.
            //
            //@param indexResults The results from an index request
            this.isEmptyOrUnfiltered = function (indexResults) {                 
                if (undefined === indexResults.unFilteredTotal ||
                    null === indexResults.unFilteredTotal) {
                    return ! indexResults.filter.isCustom();
                }
                return indexResults.unFilteredTotal === 0;
            };

            //@public
            //
            //Returns the message to display when an index request hasn't 
            //returned any results.  Either returns a message saying that there
            //aren't any matches, a message saying that there aren't any resources,
            //or a message saying that we are loading the data.
            //
            //@param indexResults The results from an index request (optional)           
            this.getEmptyMessage = function (indexResults) {
                if (indexResults &&
                    indexResults.unFilteredTotal &&
                    indexResults.unFilteredTotal > 0) {
                    return localizer.getString('core.master.noMatches');
                }
                var categoryLabel = linkTargetBuilder.
                    categoryLabel(resource.category).toLowerCase();
                if (resource.haveContacted()) {
                    return localizer.getString('core.master.noItems', [categoryLabel]);
                } else {
                    return localizer.getString('core.master.loadingItems', [categoryLabel]);
                }
            };

            /**
             * @public
             * Stop the timer polling of the index service.
             */
            this.pause = function () {
                timer = clearTimeout(timer);
                timer = null;
                location = null;
                resource.off('indexResultsChange', onIndexResultsChanged);
                resource.off('indexResultsError', onIndexResultsError);
                resource.off('selectionChange', onSelectionChanged);
                resource.off("filterChange", onFilterChange);
            };

            /**
             * @public
             * Resume the timer polling of the index service.
             */
            this.resume = function () {
                resource.on('indexResultsChange', onIndexResultsChanged);
                resource.on('indexResultsError', onIndexResultsError);
                resource.on('selectionChange', onSelectionChanged);
                resource.on("filterChange", onFilterChange);
                
                // If this page does not require selection (e.g. Activities), then
                // the behavior when it is resumed is:
                // 1. Preserve selection and filter if they exist. Scroll 
                //    to the selection just like other pages.
                // 2. If there is no prior selection, preserve the filter.
                //    Scroll to the top.
                var filter = resource.getIndexFilter();
                if (filter) {
                    if (!requireSelection && !filter.hasReferenceUri()) {
                        // If there is no selection (i.e. referenceUri does not exist in the
                        // filter), then reset the start/count and dispatch a "reset" event 
                        // so that the upper layer can adjust the page (e.g.  scrolling).
                        filter.resetToDefaults({start:true, count:true});
                        dispatcher.fire("reset");
                    }
                    var newFilter = new IndexFilter(filter);
                    if (newFilter.resetCount()) {
                        resource.setIndexFilter(newFilter);
                        resource.renotify();
                    }
                } else {
                    filter = new IndexFilter();
                    resource.setIndexFilter(filter);
                }
                
                // When resumed, we always set count to 100. If the previous indexResults 
                // continued to be used, then the logic of finding the previous selected item 
                // may succeed and locate in index 192. Then the use of referenceUri logic kicks 
                // in, but with the wrong count and wrong start. Subsequent getIndexResults() 
                // calls begin to behave erratically. The next two lines of code ensure we have 
                // a fresh start. Let the onRouteChange() and onIndexResults() do their work normally.
                lastIndexResults = null;
                resource.setIndexResults(null);
                
                if (! routePrefix) {
                    resetGetResults();
                }

                restartTimer(true);
            };

            /**
             * Use by MasterPaneView to trigger renotifying views when
             * the view is changed.
             */
            this.renotify = function (sendResults) {
                resource.renotify(sendResults);
            };

            this.haveContacted = function () {
                return resource.haveContacted();
            };

            this.haveSome = function () {
                return resource.haveSome();
            };

            this.haveMoreAbove = function () {
                return resource.haveMoreAbove();
            };
            
            this.haveMore = function () {
                return resource.haveMore();
            };

            this.hasCustomFilter = function () {
                return resource.hasCustomFilter();
            };

            this.disableUpdates = function() {
                if(timer) {
                    clearTimeout(timer);
                    timer = null;
                }
            };

            this.enableUpdates = function() {
                if(!timer) {
                    restartTimer(true);
                }
            };

            /**
             * Add a listener for a specified event.
             * @param {string} eventName The name of the event.
             * @param {function(...)}
             */
            this.on = function (eventName, callback) {
                dispatcher.on(eventName, callback);
            };

            this.off = function (eventName, callback) {
                dispatcher.off(eventName, callback);
            };
        }

        return MasterPanePresenter;
    }());

    return MasterPanePresenter;
});
