(function (global, factory) {
    if (typeof define === "function" && define.amd) {
        define([], factory);
    } else if (typeof exports !== "undefined") {
        factory();
    } else {
        var mod = {
            exports: {}
        };
        factory();
        global.bootstrapTablePipeline = mod.exports;
    }
})(this, function () {
    'use strict';

    /**
     * @author doug-the-guy
     * @version v1.0.0
     * 
     * Boostrap Table Pipeline
     * -----------------------
     *
     * This plugin enables client side data caching for server side requests which will
     * eliminate the need to issue a new request every page change. This will allow
     * for a performance balance for a large data set between returning all data at once
     * (client side paging) and a new server side request (server side paging).
     *
     * There are two new options:
     *  - usePipeline: enables this feature
     *  - pipelineSize: the size of each cache window
     *
     * The size of the pipeline must be evenly divisible by the current page size. This is
     * assured by rounding up to the nearest evenly divisible value. For example, if
     * the pipeline size is 4990 and the current page size is 25, then pipeline size will
     * be dynamically set to 5000.
     *
     * The cache windows are computed based on the pipeline size and the total number of rows
     * returned by the server side query. For example, with pipeline size 500 and total rows
     * 1300, the cache windows will be:
     *
     *  [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
     *
     * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
     * **MUST** return only the data in the requested cache window **AND** the total number of rows.
     * To wit, the server side code must use the offset and limit parameters to prepare the response
     * data.
     *
     * On a page change, the new offset is checked if it is within the current cache window. If so,
     * the requested page data is returned from the cached data set. Otherwise, a new server side
     * request will be issued for the new cache window.
     * 
     * The current cached data is only invalidated on these events:
     *  * sorting
     *  * searching
     *  * page size change
     *  * page change moves into a new cache window
     *
     * There are two new events:
     *  - cached-data-hit.bs.table: issued when cached data is used on a page change
     *  - cached-data-reset.bs.table: issued when the cached data is invalidated and a
     *      new server side request is issued
     *
     **/

    (function ($) {

        'use strict';

        var Utils = $.fn.bootstrapTable.utils;

        $.extend($.fn.bootstrapTable.defaults, {
            usePipeline: false,
            pipelineSize: 1000,
            onCachedDataHit: function onCachedDataHit(data) {
                return false;
            },
            onCachedDataReset: function onCachedDataReset(data) {
                return false;
            }
        });

        $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
            'cached-data-hit.bs.table': 'onCachedDataHit',
            'cached-data-reset.bs.table': 'onCachedDataReset'
        });

        var BootstrapTable = $.fn.bootstrapTable.Constructor,
            _init = BootstrapTable.prototype.init,
            _initServer = BootstrapTable.prototype.initServer,
            _onSearch = BootstrapTable.prototype.onSearch,
            _onSort = BootstrapTable.prototype.onSort,
            _onPageListChange = BootstrapTable.prototype.onPageListChange;

        BootstrapTable.prototype.init = function () {
            // needs to be called before initServer()  
            this.initPipeline();
            _init.apply(this, Array.prototype.slice.apply(arguments));
        };

        BootstrapTable.prototype.initPipeline = function () {
            this.cacheRequestJSON = {};
            this.cacheWindows = [];
            this.currWindow = 0;
            this.resetCache = true;
        };

        BootstrapTable.prototype.onSearch = function (event) {
            /* force a cache reset on search */
            if (this.options.usePipeline) {
                this.resetCache = true;
            }
            _onSearch.apply(this, Array.prototype.slice.apply(arguments));
        };

        BootstrapTable.prototype.onSort = function (event) {
            /* force a cache reset on sort */
            if (this.options.usePipeline) {
                this.resetCache = true;
            }
            _onSort.apply(this, Array.prototype.slice.apply(arguments));
        };

        BootstrapTable.prototype.onPageListChange = function (event) {
            /* rebuild cache window on page size change */
            var target = $(event.currentTarget);
            var newPageSize = parseInt(target.text());
            this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize);
            this.resetCache = true;
            _onPageListChange.apply(this, Array.prototype.slice.apply(arguments));
        };

        BootstrapTable.prototype.calculatePipelineSize = function (pipelineSize, pageSize) {
            /* calculate pipeline size by rounding up to the nearest value evenly divisible
             * by the pageSize */
            if (pageSize == 0) return 0;
            return Math.ceil(pipelineSize / pageSize) * pageSize;
        };

        BootstrapTable.prototype.setCacheWindows = function () {
            /* set cache windows based on the total number of rows returned by server side
             * request and the pipelineSize */
            this.cacheWindows = [];
            var numWindows = this.options.totalRows / this.options.pipelineSize;
            for (var i = 0; i <= numWindows; i++) {
                var b = i * this.options.pipelineSize;
                this.cacheWindows[i] = { 'lower': b, 'upper': b + this.options.pipelineSize - 1 };
            }
        };

        BootstrapTable.prototype.setCurrWindow = function (offset) {
            /* set the current cache window index, based on where the current offset falls */
            this.currWindow = 0;
            for (var i = 0; i < this.cacheWindows.length; i++) {
                if (this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper) {
                    this.currWindow = i;
                    break;
                }
            }
        };

        BootstrapTable.prototype.drawFromCache = function (offset, limit) {
            /* draw rows from the cache using offset and limit */
            var res = $.extend(true, {}, this.cacheRequestJSON);
            var drawStart = offset - this.cacheWindows[this.currWindow].lower;
            var drawEnd = drawStart + limit;
            res.rows = res.rows.slice(drawStart, drawEnd);
            return res;
        };

        BootstrapTable.prototype.initServer = function (silent, query, url) {
            /* determine if requested data is in cache (on paging) or if 
             * a new ajax request needs to be issued (sorting, searching, paging
             * moving outside of cached data, page size change)
             * initial version of this extension will entirely override base initServer 
             **/

            var data = {};
            var index = this.header.fields.indexOf(this.options.sortName);

            var params = {
                searchText: this.searchText,
                sortName: this.options.sortName,
                sortOrder: this.options.sortOrder
            };

            var request = null;

            if (this.header.sortNames[index]) {
                params.sortName = this.header.sortNames[index];
            }

            if (this.options.pagination && this.options.sidePagination === 'server') {
                params.pageSize = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize;
                params.pageNumber = this.options.pageNumber;
            }

            if (!(url || this.options.url) && !this.options.ajax) {
                return;
            }

            var useAjax = true;
            if (this.options.queryParamsType === 'limit') {
                params = {
                    searchText: params.searchText,
                    sortName: params.sortName,
                    sortOrder: params.sortOrder
                };
                if (this.options.pagination && this.options.sidePagination === 'server') {
                    params.limit = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize;
                    params.offset = (this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize) * (this.options.pageNumber - 1);
                    if (this.options.usePipeline) {
                        // if cacheWindows is empty, this is the initial request
                        if (!this.cacheWindows.length) {
                            useAjax = true;
                            params.drawOffset = params.offset;
                            // cache exists: determine if the page request is entirely within the current cached window
                        } else {
                            var w = this.cacheWindows[this.currWindow];
                            // case 1: reset cache but stay within current window (e.g. column sort)
                            // case 2: move outside of the current window (e.g. search or paging)
                            //  since each cache window is aligned with the current page size
                            //  checking if params.offset is outside the current window is sufficient.
                            //  need to requery for preceding or succeeding cache window
                            //  also handle case 
                            if (this.resetCache || params.offset < w.lower || params.offset > w.upper) {
                                useAjax = true;
                                this.setCurrWindow(params.offset);
                                // store the relative offset for drawing the page data afterwards
                                params.drawOffset = params.offset;
                                // now set params.offset to the lower bound of the new cache window
                                // the server will return that whole cache window
                                params.offset = this.cacheWindows[this.currWindow].lower;
                                // within current cache window
                            } else {
                                useAjax = false;
                            }
                        }
                    } else {
                        if (params.limit === 0) {
                            delete params.limit;
                        }
                    }
                }
            }

            // force an ajax call - this is on search, sort or page size change
            if (this.resetCache) {
                useAjax = true;
                this.resetCache = false;
            }

            if (this.options.usePipeline && useAjax) {
                /* in this scenario limit is used on the server to get the cache window
                 * and drawLimit is used to get the page data afterwards */
                params.drawLimit = params.limit;
                params.limit = this.options.pipelineSize;
            }

            // cached results can be used
            if (!useAjax) {
                var res = this.drawFromCache(params.offset, params.limit);
                this.load(res);
                this.trigger('load-success', res);
                this.trigger('cached-data-hit', res);
                return;
            }
            // cached results can't be used
            // continue base initServer code    
            if (!$.isEmptyObject(this.filterColumnsPartial)) {
                params.filter = JSON.stringify(this.filterColumnsPartial, null);
            }

            data = Utils.calculateObjectValue(this.options, this.options.queryParams, [params], data);

            $.extend(data, query || {});

            // false to stop request
            if (data === false) {
                return;
            }

            if (!silent) {
                this.$tableLoading.show();
            }
            var self = this;

            request = $.extend({}, Utils.calculateObjectValue(null, this.options.ajaxOptions), {
                type: this.options.method,
                url: url || this.options.url,
                data: this.options.contentType === 'application/json' && this.options.method === 'post' ? JSON.stringify(data) : data,
                cache: this.options.cache,
                contentType: this.options.contentType,
                dataType: this.options.dataType,
                success: function success(res) {
                    res = Utils.calculateObjectValue(self.options, self.options.responseHandler, [res], res);
                    // cache results if using pipelining
                    if (self.options.usePipeline) {
                        // store entire request in cache
                        self.cacheRequestJSON = $.extend(true, {}, res);
                        // this gets set in load() also but needs to be set before
                        // setting cacheWindows
                        self.options.totalRows = res[self.options.totalField];
                        // if this is a search, potentially less results will be returned
                        // so cache windows need to be rebuilt. Otherwise it
                        // will come out the same
                        self.setCacheWindows();
                        self.setCurrWindow(params.drawOffset);
                        // just load data for the page
                        res = self.drawFromCache(params.drawOffset, params.drawLimit);
                        self.trigger('cached-data-reset', res);
                    }
                    self.load(res);
                    self.trigger('load-success', res);
                    if (!silent) self.$tableLoading.hide();
                },
                error: function error(res) {
                    var data = [];
                    if (self.options.sidePagination === 'server') {
                        data = {};
                        data[self.options.totalField] = 0;
                        data[self.options.dataField] = [];
                    }
                    self.load(data);
                    self.trigger('load-error', res.status, res);
                    if (!silent) self.$tableLoading.hide();
                }
            });

            if (this.options.ajax) {
                Utils.calculateObjectValue(this, this.options.ajax, [request], null);
            } else {
                if (this._xhr && this._xhr.readyState !== 4) {
                    this._xhr.abort();
                }
                this._xhr = $.ajax(request);
            }
        };

        $.fn.bootstrapTable.methods.push();
    })(jQuery);
});