// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.
/*global Modernizr localStorage*/
define(['hp/core/EventDispatcher',
    'hp/services/IndexService',
    'hp/services/IndexFilter',
    'hp/services/ResourceService',
    'hp/model/DevelopmentSettings',
    'hp/services/REST',
    'hp/services/Log',
    'jquery'],
function (EventDispatcher, indexService, IndexFilter, resourceService, settings, REST, log, jquery) {
"use strict";

    /*
     * Compare index results in the fastest possible manner.
     *   If compareResources is not a function all comparisons are false
     *   If lastIndexResults or nextIndexResults are undefined, or of differing sizes the comparison is false
     *   If all members within both result sets are the same (including order) the comparison is true
     *
     * If the return from this function is false, tables will be re-rendered through onIndexResultChanged.
     *
     * This routine is declared outside the module because this function does not need access to the modules
     *   instance variables.  This routine is common for any resource.
     *   This helps cut down on memory leaked due to closures.
     */
    function indexResultSetComparison(compareResources, lastIndexResults, nextIndexResults) {
        if (typeof compareResources !== "function") {
            return false;
        }

        if (lastIndexResults && nextIndexResults &&
                lastIndexResults.count === nextIndexResults.count &&
                lastIndexResults.members && nextIndexResults.members &&
                lastIndexResults.members.length === nextIndexResults.members.length) {
            /*
             * Cache both array values locally.  Iteration in reverse is allowed and optimal in this case.
             * 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 lastMembers = lastIndexResults.members;
            var nextMembers = nextIndexResults.members;

            for (var i = lastMembers.length; i--;) {
                if (! compareResources(lastMembers[i], nextMembers[i])) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /*
     * We need to call garbage collection if the routine exists to help stabalize the memory footprint on IE8.
     *   CollectGarbage is not be defined on non IE browsers, if its not this routine does nothing.
     */
    /*global CollectGarbage*/
    function doGarbageCollection() {
        if (typeof(CollectGarbage) === "function") {
            CollectGarbage();
        }
    }

    var Resource = (function () {

        // Used for tracking async calls. The ordering of these values is significant to be processed
        // by Math.max(). See getItemsImpl() for more details.
        var SUCCESS = 1, FAILURE = 2, WAITING = 3,
            BATCH_SIZE = 5;

        /**
         * @constructor
         * @type {Resource}
         * @param indexCategory The category name to request from index
         * @param resourceOptions An object containing options. Potential options can be 0 or more of:
         *     assocNames: also pull the specified associations from index. This could be single
         *           association name or an array of association names. e.g. 'BLADE_TO_PROFILE' or
         *           ['ENCLOSURE_TO_BLADE','BLADE_TO_PROFILE']
         *     resourceUri: The base REST URI to use rather than using index to get items. For example:
         *           /taskt/rest/resources
         *     getResourceCollectionMethod: a function to call to get the collection of resources instead
         *           of calling index or resourceUri. This function signature is function(options) where
         *           options is an object with 0 or more of the following:
         *                  assocNames : The association names above
         *                  resourceUri : The resourceUri above
         *                  filter : An IndexFilter object containing the filter, sort and paging parameters
         *                  success : function(data) - the function to call on success with the data
         *                  error : function(errorInfo) - the cuntion to call on failure
         *     existsInFilterMethod: a function to call to verify the uris can be found in the filtered set.
         *           If the resource is not wired with the index service, then it must provide this method.
         *           If not provided, the default implementation makes a REST call to the resource manager to
         *           retrieve the item represented by the URI, and ignores the filter argument.
         *           The function signature is function (uris, filter, handlers) where
         *                  uris : An array containing the target uris to find
         *                  filter: An IndexFilter object containing the filter, sort and paging parameters
         *                  handlers.success : function ({total:found}) - the function to call on success. Its
         *                                     argument is an object with "total" property set to the number of
         *                                     uris found
         *                  handlers.error : function(errorInfo) - the function to call on failure.
         */
        function Resource(indexCategory, resourceOptions) {

            // Derive from EventDispatcher
            EventDispatcher.call(this);

            // vars in the constructor basically end up being private instance variables
            // for the class due to the closure they end up in.
            var dispatcher = this,
                filter = null, // IndexFilter
                lastIndexResults = null, // cache to support getIndexResultForUri
                selectedUris = [],
                priorSelectedUris = [],
                priorMultiSelectId = null,
                postSelectedUri,
                multiSelectSets = [[]], // empty first item
                selectedMultiSelectId = null,
                indexPropsToMonitor = ['state'],
                needItemUpdate = false,
                selectedItems = {}, // uri -> item, lazily loaded when asked for
                removingMap = {}, // map of uris being removed
                opts = {
                    category: indexCategory, // can be an array of multiple categories
                    resourceUri: null, // base URI for getting resource collections from RMs rather than index
                    assocNames: null,
                    getResourceCollectionMethod: null, // function to do the work of getting a list of resources
                    existsInFilterMethod: null, // function to check the existence of an uri in a filtered result
                    postFilter: null // function to filter results before advertising them
                };

            // Look for the item with the specified uri. If found,
            // return an object containing the item.
            function getIndexResultForUriImpl(uri) {
                var indexResult = null;
                if (lastIndexResults) {
                    // Perhaps some day make this a hash for speed
                    $.each(lastIndexResults.members, function (index, result) {
                        if (uri === result.uri) {
                            indexResult = result;
                            indexResult.index = index;
                            return false;
                        }
                    });
                }
                return indexResult;
            }

            function arePropertiesDifferent(indexResult, item, properties) {
                var i,
                    property,
                    different = false,
                    idxPropValue;
                for (i = 0; i < properties.length; i++) {
                    property = properties[i];
                    if (item.hasOwnProperty(property)) {
                        idxPropValue = null;
                        if (indexResult.hasOwnProperty(property)) {
                            idxPropValue = indexResult[property];
                        } else if (indexResult.attributes &&
                            indexResult.attributes.hasOwnProperty(property)) {
                            //
                            // Also honor the property in the attributes list
                            //
                            idxPropValue = indexResult.attributes[property];
                        }
                        if (idxPropValue && (idxPropValue !== item[property])) {
                            different = true;
                            break;
                        }
                    }
                }
                return different;
            }

            function haveSelectedIndexResultsChanged(uris) {
                var item,
                    indexResult,
                    differences = false;

                if (uris) {
                    $.each(uris, function (index, uri) {
                        if (selectedItems[uri]) {
                            item = selectedItems[uri];
                            indexResult = getIndexResultForUriImpl(uri);
                            if (indexResult) {
                                if (arePropertiesDifferent(indexResult, item, indexPropsToMonitor)) {
                                    selectedItems[uri] = null;
                                    differences = true;
                                }
                            }
                        }
                    });
                }
                //log.info('haveSelectedIndexResultsChanged = ' + differences);
                return differences;
            }

            function checkForChanges() {
                if (haveSelectedIndexResultsChanged(selectedUris)) {
                    needItemUpdate = true;
                    window.setTimeout(function () {
                        if (needItemUpdate) {
                            //log.info("updating selection based on changed attributes");
                            dispatcher.fire("selectionChange",
                                    {uris: selectedUris, multiSelectId: selectedMultiSelectId});
                        }
                    }, 10);
                }
            }

            function onGetResourcesSuccess(data, filterUsed, handlers) {
                // Track how much data is returned before postFilter is applied
                data.__preFilteredCount = data.count;
                if (opts.postFilter) {
                    opts.postFilter(data);
                }
                var sameDataset = indexResultSetComparison(opts.compareResources, lastIndexResults, data);
                data.filter = filterUsed;

                if (data.filter) {
                    data.filter.data.start = data.start;
                } else if (data.start === undefined) {
                    data.start = 0;
                }
                
                if (! sameDataset) {
                    lastIndexResults = data;
                    checkForChanges();
                }

                /*
                 * We call handlers.success whenever we successfully retrieve a dataset.
                 *   We have to do this because we end up re-triggering the timer event through the handlers.
                 *   Resource.js has no concept of timed events, and thus does not have the ability to reset
                 *     the timer function.
                 */
                if (handlers && handlers.success) {
                    handlers.success(lastIndexResults);
                }

                if (! sameDataset) {
                    dispatcher.fire("indexResultsChange", lastIndexResults);
                }

                doGarbageCollection();
            }

            function onGetResourcesError(errorInfo, filterUsed, handlers) {
                if (handlers && handlers.error) {
                    handlers.error(errorInfo.errorMessage);
                }
                dispatcher.fire("indexResultsError", errorInfo);
            }

            /*
             * This function is not anonymous, because it only needs to wrap the handler pointer when it
             *   creates the success and error closures.
             */
            function getHandlers(filterUsed, handlers) {
                return { filter: filterUsed,
                    success: function (data) {
                        onGetResourcesSuccess(data, filterUsed, handlers);
                    },
                    error: function (errorInfo) {
                        onGetResourcesError(errorInfo, filterUsed, handlers);
                    }
                };
            }

            function buildTreeIndexResult(data, filterUsed) {
                var tmpIndexResults = {members: [], count: 0, total: 0,
                    start: (filterUsed ? filterUsed.data.start : 0)};
                var members;
                if (data) {
                    tmpIndexResults.count = data.total;
                    if (data.hasOwnProperty('members')) {
                        tmpIndexResults.count = data.count;
                        members = data.members;
                    }
                    else {
                        tmpIndexResults.count = data.total;
                        members = data.trees;
                    }
                    if (data.hasOwnProperty('unFilteredTotal')) {
                        tmpIndexResults.unFilteredTotal = data.unFilteredTotal;
                    }
                    tmpIndexResults.total = data.total;
                    $.each(members, function(index, tree) {
                        var tmpResource = tree.resource;
                        tmpResource.children = tree.children;
                        tmpResource.parents = tree.parents;
                        tmpIndexResults.members.push(tmpResource);
                    });
                }
                return tmpIndexResults;
            }

            function getTrees(assocNames, filterUsed, handlers) {
                var myHandlers = {
                    success: function (data) {
                        onGetResourcesSuccess(buildTreeIndexResult(data, filterUsed), filterUsed, handlers);
                    },
                    error: function (errorInfo) {
                        onGetResourcesError(errorInfo, filterUsed, handlers);
                    }
                };
                
                var opts = {
                        filter: filterUsed,
                        childDepth : "1",
                        parentDepth : "1",
                        handlers : myHandlers
                };
                
                // if filter does not provide sorting info, set the default.
                if (!(filterUsed && filterUsed.data && filterUsed.data.sort)) {
                    opts.sort = "name:asc";
                }
                
                indexService.getParentAndChildrenAssociations(opts);
            }

            function matchesCategory(aCategory) {
                if ($.isArray(opts.category)) {
                    return ($.inArray(aCategory, opts.category) !== -1);
                } else {
                    return (aCategory === opts.category);
                }
            }

            function getResources(options) {
                resourceService.getFilteredResources(options.resourceUri, options.filter, {
                    success: options.success,
                    error: options.error
                });
            }

            // DEPRECATED:
            // This method is written to support existsInFilterMethodImpl(), which
            // will be deprecated after all partners' resources are integrated
            // with the index service.
            function processNextBatch(handlers) {
                var batchUris = handlers.getUrisArray().splice(0, BATCH_SIZE);
                handlers.setBatchLength(batchUris.length);
                $.each(batchUris, function (index, uri) {
                    REST.getURI(uri, handlers);
                });
            }

            // DEPRECATED:
            // This method is written to support existsInFilterMethodImpl(), which
            // will be deprecated after all partners' resources are integrated
            // with the index service.
            function existsInFilterHandlers(uris, handlers) {
                var urisArray = [];
                var total = uris.length;
                var foundCount = 0;
                var batchLength = 0;
                var done = false;
                $.extend(urisArray, uris);
                var thisHandler = {
                    getUrisArray: function() {
                        return urisArray;
                    },
                    setBatchLength: function (len) {
                        batchLength = len;
                    },
                    success : function (data, status, xhr) {
                        if (!done) {
                            --batchLength;
                            ++foundCount;
                            done = (urisArray.length === 0) &&
                                (batchLength === 0);
                            if ((foundCount === total) || done) {
                                if (handlers && handlers.success) {
                                     handlers.success({total:foundCount});
                                }
                            } else if (batchLength === 0) {
                                processNextBatch(thisHandler);
                            }
                        }
                    },
                    error : function (errorInfo, xhr) {
                        if (!done) {
                            done = true;
                            if (handlers && handlers.error) {
                                handlers.error(errorInfo);
                            }
                        }
                    }
                };
                return thisHandler;
            }

            // DEPRECATED:
            // This default implementation for the existsInFilterMethod method
            // will be deprecated after all partners' resources are integrated
            // with the index service.
            function existsInFilterMethodImpl(uri, aFilter, handlers) {
                processNextBatch(existsInFilterHandlers(uri, handlers));
            }

            /**
             * @private
             * Call the index service to get search results for the category.
             * See onGetItemsSuccess and onGetItemsError
             */
            function getIndexResults(filterUsed, handlers) {
                var myHandlers = getHandlers(filterUsed, handlers);

                if (filterUsed && filterUsed.data.associationName) {
                    indexService.getFilteredAssociations(filterUsed, {
                            success: function (associations) {
                                // If index service API version 1 is used,
                                // prune down to ones matching my category
                                if (indexService.version() === 1) {
                                    associations = $.grep(associations, function (assoc) {
                                        if (filter.data.startObjUri) {
                                            return matchesCategory(assoc.endObj.category);
                                        } else {
                                            return matchesCategory(assoc.startObj.category);
                                        }
                                    });

                                    myHandlers.success({
                                        total: associations.length,
                                        count: associations.length,
                                        members: $.map(associations, function (assoc, index) {
                                            if (filter.data.startObjUri) {
                                                return assoc.endObj;
                                            } else {
                                                return assoc.startObj;
                                            }
                                        })
                                    });
                                } else {
                                    myHandlers.success({
                                        total: associations.total,
                                        unFilteredTotal: associations.unFilteredTotal,
                                        count: associations.count,
                                        start: filterUsed.data.start ? filterUsed.data.start : 0,
                                        members: $.map(associations.members, function (assoc, index) {
                                            if (filter.data.startObjUri) {
                                                return assoc.childResource;
                                            } else {
                                                return assoc.parentResource;
                                            }
                                        })
                                    });
                                }
                            },
                            error: myHandlers.error
                        });
                } else if (filterUsed) {
                    indexService.getFilteredIndexResources(filterUsed, myHandlers);
                } else {
                    indexService.getIndexResources(opts.category, 0,
                        settings.getMaxIndexItems(), myHandlers);
                }
            }

            function getItemImpl(uri, handlers, forceGet, options) {
                if (selectedItems[uri] && !forceGet) {
                    if (handlers && handlers.success) {
                        handlers.success(selectedItems[uri]);
                    }
                } else {
                    REST.getURI(uri,
                        {
                            success : function (data, status, xhr) {
                                selectedItems[uri] = data;
                                if (handlers && handlers.success) {
                                    handlers.success(data, status, xhr);
                                }
                                dispatcher.fire('itemChange', data);
                            },
                            error : function (errorInfo, xhr) {
                                log.warn(errorInfo.errorMessage);
                                if (!removingMap.hasOwnProperty(uri)) {
                                    if (handlers && handlers.error) {
                                        handlers.error(errorInfo.errorMessage, errorInfo, xhr);
                                    }
                                    dispatcher.fire('itemError', errorInfo);
                                }
                            }
                        },
                    options);
                }
            }

            function fireAggregateGetItemsResultHandlers(uris, handlers, results, errorInfos) {
                if (handlers) {
                    var i, result, aggregateResult;
                    for (i = 0; i < uris.length; i++) {
                        result = results.hasOwnProperty(uris[i]) ? results[uris[i]] : WAITING;
                        aggregateResult = aggregateResult ? Math.max(aggregateResult, result) : result;
                    }
                    if ((aggregateResult === SUCCESS) && handlers.success) {
                        handlers.success(selectedItems);
                    } else if ((aggregateResult === FAILURE) && handlers.error) {
                        handlers.error(errorInfos);
                    }
                }
            }

            function getItemsImpl(uris, handlers, forceGet, options) {
                var results = {}, errorInfos = {};
                if (uris) {
                    $.each(uris, function (index, uri) {
                        getItemImpl(uri, {
                            success : function (data, status, xhr) {
                                results[uri] = SUCCESS;
                                fireAggregateGetItemsResultHandlers(uris, handlers, results, errorInfos);
                            },
                            error : function (errorInfo, xhr) {
                                results[uri] = FAILURE;
                                errorInfos[uri] = errorInfo;
                                fireAggregateGetItemsResultHandlers(uris, handlers, results, errorInfos);
                            }
                        }, forceGet, options);
                    });
                }
            }

            function addIndexPropsToMonitor(additionalProps) {
                indexPropsToMonitor.push(additionalProps);
            }

            function setPostSelection(selectedUris) {
                postSelectedUri = null;
                if (lastIndexResults && selectedUris && selectedUris.length > 0) {
                    var isPreviousItemSelected = false;
                    var selectedUriFound = false;
                    var lastNotSelectedUri;
                    var possiblePostSelectedUri;
                    $.each(lastIndexResults.members, function (index, indexResult) {
                        if ((indexResult) &&
                            ($.inArray(indexResult.uri, selectedUris) !== -1)) {
                            isPreviousItemSelected = true;
                            selectedUriFound = true;
                            // The selected item is found. Next is to find the
                            // first not-selected item that comes after.
                            possiblePostSelectedUri = null;
                        } else if (isPreviousItemSelected) {
                            // The first not-selected item that comes after
                            // a selected item is found.
                            possiblePostSelectedUri = indexResult.uri;
                            isPreviousItemSelected = false;
                        } else {
                            // Keep a reference to the latest not selected item
                            // just in case the last item is removed.
                            lastNotSelectedUri = indexResult.uri;
                        }
                    });
                    // Note this not resetting the selection, but rather providing
                    // a pointer to restore selection later.  It is the presenter's
                    // delete handler's responsibility to restore selection.
                    // Mainly for deletion: store the next element uri ready to be
                    // selected if the previous element is removed.
                    if (possiblePostSelectedUri) {
                        postSelectedUri = possiblePostSelectedUri;
                    } else if (selectedUriFound && lastNotSelectedUri) {
                        postSelectedUri = lastNotSelectedUri;
                    } else {
                        // The selectedUri may not be in lastIndexResult, or there
                        // is nothing available to be post-selected.
                        postSelectedUri = null;
                    }
                }
            }

            function isSelectedUrisUpdateNeeded(uris, multiSelectId) {
                return !selectedUris || (!uris && selectedUris) ||
                    uris.length !== selectedUris.length ||
                    uris[0] !== selectedUris[0] ||
                    (multiSelectId && (multiSelectId !== selectedMultiSelectId));
            }

            function setSelectedUrisImpl(uris, multiSelectId, dropPriorSelections) {
                if ((!uris) || (uris && uris.length < 1 && filter)) {
                    // if selected uris is not set, then we must not use referenceUri
                    filter.setReferenceUri(null);
                }

                if (isSelectedUrisUpdateNeeded(uris, multiSelectId)) {
                    priorSelectedUris = (dropPriorSelections ? [] : selectedUris);
                    priorMultiSelectId = (dropPriorSelections ? null : selectedMultiSelectId);
                    selectedUris = (uris ? uris : []);
                    selectedItems = {};
                    needItemUpdate = false;
                    setPostSelection(selectedUris);

                    if (selectedUris.length > 1) {
                        if (! multiSelectId) {
                            multiSelectSets.push(selectedUris);
                            multiSelectId = multiSelectSets.length - 1;

                            // store latest multiple selection in local storage
                            if (Modernizr.localstorage) {
                                localStorage.setItem(
                                    "hp.ui." + opts.category + ".multiselect.index",
                                    multiSelectId);
                                localStorage.setItem(
                                    "hp.ui." + opts.category + ".multiselect.uris",
                                    selectedUris.join(','));
                            }
                        }
                        selectedMultiSelectId = multiSelectId;
                        dispatcher.fire("selectionChange",
                            {uris: selectedUris, multiSelectId: multiSelectId});
                    } else {
                        selectedMultiSelectId = null;
                        dispatcher.fire("selectionChange", {uris: selectedUris});
                    }
                    return true;
                }
                else {
                    if (haveSelectedIndexResultsChanged(uris)) {
                        needItemUpdate = false;
                        dispatcher.fire("selectionChange",
                            {uris: selectedUris, multiSelectId: multiSelectId});
                    }
                    return false;
                }
            }

            function getResourceCollection(handlers) {
                var filterUsed = filter;
                if (filter && filter.data && filter.data.start === undefined) {
                    filter.data.start = 0;
                }
                if (opts.getResourceCollectionMethod) {
                    //In this case we grab the default handler object and augment it with additional fields
                    // before we call the custom resourceCollection method.
                    var myHandlers = getHandlers(filterUsed, handlers);
                    myHandlers.assocNames = opts.assocNames;
                    myHandlers.resourceUri = opts.resourceUri;
                    myHandlers.filter = filterUsed;
                    opts.getResourceCollectionMethod(myHandlers);
                }
                else if (opts.assocNames) {
                    getTrees(opts.assocNames, filterUsed, handlers);
                }
                else {
                    getIndexResults(filterUsed, handlers);
                }
            }

            function init() {
                if (resourceOptions) {
                    if ((typeof(resourceOptions) === 'object') && ! $.isArray(resourceOptions)) {
                        $.extend(true, opts, resourceOptions);

                        if (opts.resourceUri) {
                            if (! opts.getResourceCollectionMethod) {
                                opts.getResourceCollectionMethod = getResources;
                            }
                            if (! opts.existsInFilterMethod) {
                                log.warn('Resource.existsInFilterMethod() implementation is not provided; ' +
                                         'default implementation is used.');
                                opts.existsInFilterMethod = existsInFilterMethodImpl;
                            }
                        }
                    }
                    else {
                        log.warn('Resource assocNames parameter is deprecated and will be remove in sprint 24. '+
                                 'Change to using the options parameter');
                        opts.assocNames = resourceOptions; // old style string arg for assocNames
                    }
                }
            }

            // Make these public
            this.category = opts.category;

            this.getIndexResults = getResourceCollection;

            this.getIndexFilter = function () {
                return filter;
            };

            this.setIndexFilter = function(newFilter, forceNewFilter) {
                var start = 0;
                var count = settings.getMaxIndexItems();

                if (newFilter.defaults) {
                    if (newFilter.defaults.hasOwnProperty('count')) {
                        count = newFilter.defaults.count;
                    }
                    if (newFilter.defaults.hasOwnProperty('start')) {
                        start = newFilter.defaults.start;
                    }
                }
                newFilter.copyHidden(filter);
                newFilter.ensureDefaults(opts.category, start, count);

                if (forceNewFilter || newFilter.isDifferent(filter)) {
                    filter = newFilter;
                    dispatcher.fire("filterChange", filter);
                    return true;
                } else {
                    // at least get our defaults in there if we don't have them
                    if (! filter) {
                        filter = newFilter;
                    }
                    return false;
                }
            };

            /**
             * @public
             * Republish index results and selection events.
             */
            this.renotify = function (sendResults) {
                if (selectedUris) {
                    dispatcher.fire("selectionChange", {
                        uris: selectedUris,
                        multiSelectId: selectedMultiSelectId
                    });
                }
                if (sendResults && lastIndexResults) {
                    dispatcher.fire("indexResultsChange", lastIndexResults);
                }
            };

            // Would like to deprecate this function,
            // does ICModuleResource really need it?
            this.setIndexResults = function (results) {
                lastIndexResults = results;
            };

            // Return the best suited uri as the selection
            this.getBestSelectionUri = function () {
                return (lastIndexResults && lastIndexResults.members.length > 0) ? lastIndexResults.members[0].uri : null;
            };

            this.updateIndexResult = function (newResult) {
                if (lastIndexResults) {
                    // see if we have an index result for the same uri
                    $.each(lastIndexResults.members, function (index, result) {
                        if (result.uri === newResult.uri) {
                            lastIndexResults.members[index] = newResult;
                            dispatcher.fire("indexResultsChange", lastIndexResults);
                            return false;
                        }
                    });
                }
            };

            this.haveContacted = function () {
                return (lastIndexResults !== null);
            };

            this.haveSome = function () {
                return (lastIndexResults ? lastIndexResults.total > 0 : false);
            };

            this.haveMoreAbove = function () {
                return ((lastIndexResults && lastIndexResults.members.length > 0) ?
                        (lastIndexResults.start > 0): false);
            };

            this.haveMore = function () {
                return ((lastIndexResults && lastIndexResults.members.length > 0) ?
                    (lastIndexResults.start + lastIndexResults.count < lastIndexResults.total) : false);
            };

            this.hasCustomFilter = function () {
                return (filter ? filter.isCustom() : false);
            };

            this.haveRequestedMore = function () {
                // check the number of items returned in the lastIndexResults before postFilter is applied.
                return (this.haveMore() && (filter.data.count > lastIndexResults.__preFilteredCount));
            };

            // Return the start index from the last result
            this.getStart = function() {
                return (lastIndexResults ? lastIndexResults.start : -1);
            };

            // Update POST selection
            this.setPostSelection = setPostSelection;

            /**
             * @public
             * Get the last index result available for this URI.
             * This function can return undefined if there is
             * no index result available. That doesn't mean the resource
             * doesn't exist on the appliance, just that it isn't in the
             * last index results cache.
             */
            this.getIndexResultForUri = getIndexResultForUriImpl;

            /**
             * @public
             * Check the existence of the resource represented by the uri in the filtered set.
             *
             * @param uris An array containing the target uris to find
             * @param filter An IndexFilter object containing the filter, sort and paging paramenters
             * @param handlers success and error callback functions
             */
            this.existsInFilter = function(uris, aFilter, handlers) {
                var myFilter = new IndexFilter(aFilter);
                if (opts.existsInFilterMethod) {
                    opts.existsInFilterMethod(uris, myFilter, handlers);
                } else {
                    try {
                        myFilter.setProperty('_uri', uris);
                    } catch (errofInfo) {}
                    myFilter.data.count = 0;
                    indexService.checkForExistence(myFilter, handlers);
                }
            };

            /**
             * @public
             *
             * @param {Object} The current selection {uris: Array, multiSelectId: String}
             */
            this.getSelection = function () {
                return {uris: selectedUris, multiSelectId: selectedMultiSelectId};
            };

            /**
            * @public
            *
            * @param {String} id The multiselect id
            *
            * @return {Array}  The array of object URIs represented by the multiselect id
            */
            this.getMultiSelectedUris = function (id) {
                return multiSelectSets[id];
            };

            /**
             * @public
             *
             * Invalidate the multiselect id. It represents an invalid set of uris
             *
             * @param {String} id The multiselect id
             *
             */
            this.invalidateMultiSelectId = function (id) {
                delete multiSelectSets[id];
            };

            /**
             * @public
             *
             * @return {Object} The prior selection {uris: Array, multiSelectId: String}
             */
            this.getPriorSelection = function () {
                return {uris: priorSelectedUris, multiSelectId: priorMultiSelectId};
            };

            /**
             * @public
             * Set the list of selected objects from URIs.
             * Retrieves the object for first URI in the list from the backend RM
             * and sets the current item to that object (fires "currentItemChanged" event.)
             *
             * @param {Array} aUris The array of object URIs to consider selected
             */
            this.setSelectedUris = setSelectedUrisImpl;

            this.addIndexPropsToMonitor = addIndexPropsToMonitor;

            this.selectMultiSelectId = function (multiSelectId) {
                var found = false;
                var storedMultiSelectId;

                if (multiSelectId < multiSelectSets.length) {
                    if (multiSelectSets[multiSelectId]) {
                        setSelectedUrisImpl(multiSelectSets[multiSelectId], multiSelectId);
                        found = true;
                    }
                } else {
                    // if we have local storage and this matches the saved index, use it
                    if (Modernizr.localstorage) {

                        storedMultiSelectId = parseInt(localStorage.getItem(
                            "hp.ui." + opts.category + ".multiselect.index"), 10);

                        if (storedMultiSelectId === multiSelectId) {

                            multiSelectSets.push(localStorage.getItem(
                                "hp.ui." + opts.category + ".multiselect.uris").split(','));
                            multiSelectId = multiSelectSets.length - 1;
                            // reset the index since we changed it
                            localStorage.setItem(
                                "hp.ui." + opts.category + ".multiselect.index",
                                multiSelectId);

                            setSelectedUrisImpl(multiSelectSets[multiSelectId], multiSelectId);
                            found = true;
                        }
                    }
                }

                if (! found) {
                    selectedUris = [];
                    selectedMultiSelectId =  null;
                }

                return found;
            };

            this.clearSelectedUris = function () {
                setSelectedUrisImpl([]);
            };

            this.clearAllSelectedUris = function (makePostSelection) {
                var selection = postSelectedUri;
                setSelectedUrisImpl([], null, true);
                if (makePostSelection && selection) {
                    setSelectedUrisImpl([selection]);
                }
            };

            this.removeSelectedUri = function (uri) {
                var initialLength = selectedUris.length;

                if (uri) {
                    selectedUris = $.grep(selectedUris, function(selectedUri) {
                        return selectedUri !== uri;
                    });
                    priorSelectedUris = $.grep(priorSelectedUris, function(selectedUri) {
                        return selectedUri !== uri;
                    });
                    if (selectedItems[uri]) {
                        delete selectedItems[uri];
                    }
                    if (selectedUris.length !== initialLength) {
                        needItemUpdate = false;
                        dispatcher.fire("selectionChange", {uris: selectedUris});
                    }
                }
            };

            this.removeSelectedUris = function () {
                var olderUris = priorSelectedUris;
                setSelectedUrisImpl([]);
                priorSelectedUris = olderUris;
            };

            this.selectContiguousToUri = function (uri) {
                var beginIndex = -1;
                var endIndex = -1;
                var index;
                var newUris = selectedUris.slice(0);

                if (lastIndexResults) {

                    var alreadySelected = ($.inArray(uri, selectedUris) !== -1);

                    // figure out bounding indexes
                    $.each(lastIndexResults.members, function (index, result) {
                        if (uri === result.uri) {
                            // if we've already got a beginning, we're done
                            if (-1 !== beginIndex) {
                                endIndex = (alreadySelected ? (index - 1) :
                                    index);
                                return false;
                            } else {
                                endIndex = (alreadySelected ? (index + 1) :
                                    index);
                            }
                        }
                        if ($.inArray(result.uri, selectedUris) !== -1) {
                            if (-1 === endIndex) {
                                // before new uri, (re)set beginning
                                beginIndex = (index + 1);
                            } else {
                                // after new uri, swap begin/end and we're done
                                beginIndex = endIndex;
                                endIndex = (index - 1);
                                return false;
                            }
                        }
                    });

                    // select between indexes
                    for (index = beginIndex; index <= endIndex; index += 1) {
                        newUris.push(lastIndexResults.members[index].uri);
                    }

                    setSelectedUrisImpl(newUris);
                }
            };

            this.getPriorSelectedUris = function () {
                return priorSelectedUris;
            };

            this.getPostSelectedUri = function () {
                return postSelectedUri;
            };

            this.getItem = getItemImpl;

            this.getItems = getItemsImpl;

            /**
             * Tag a URI as currently in the process of being removed. This causes getItem() to not generate an 'itemError'
             * event if it gets an error while trying to get the specified uri.
             * The caller should also call doneBeingRemoved(uri) when the specified item is finished being removed.
             *
             * See isBeingRemoved() and doneBeingRemoved().
             */
            this.addBeingRemoved = function(uri) {
                removingMap[uri] = uri;
            };

            /**
             * Returns true if the URI was previously tagged as being removed via addBeingRemoved(uri)
             */
            this.isBeingRemoved = function(uri) {
                return removingMap.hasOwnProperty(uri);
            };

            /**
             * Untags the specified URI as being removed.
             */
            this.doneBeingRemoved = function(uri) {
                if (removingMap.hasOwnProperty(uri)) {
                    delete removingMap[uri];
                }
            };

            /**
             * When an action generates a task, this sends and event to
             * let the notification are know that it should update what it has.
             */
            this.notifyActivityChange = function (item) {
                dispatcher.fire('activityChange', item);
            };

            /*
             * Used in test code to disable the default resource comparison function
             */
            this.setCompareResourcesFunction = function (compareResources) {
                opts.compareResources = compareResources;
            };

            init();
        }

        return Resource;
    }());

    return Resource;
});
