/*
 * pagination.js 2.5.0
 * A jQuery plugin to provide simple yet fully customisable pagination.
 * https://github.com/superRaytin/paginationjs
 *
 * Homepage: http://pagination.js.org
 *
 * Copyright 2014-2100, superRaytin
 * Released under the MIT license.
 */

(function(global, $) {

  if (typeof $ === 'undefined') {
    throwError('Pagination requires jQuery.');
  }

  var pluginName = 'pagination';

  var pluginHookMethod = 'addHook';

  var eventPrefix = '__pagination-';

  if ($.fn.pagination) {
    throwError('plugin conflicted, the name "pagination" has been taken by another jQuery plugin.');
  }

  $.fn[pluginName] = function(options) {

    if (typeof options === 'undefined') {
      return this;
    }

    var container = $(this);

    var attributes = $.extend({}, $.fn[pluginName].defaults, options);

    var pagination = {

      initialize: function() {
        var self = this;

        // Cache data for current instance
        if (!container.data('pagination')) {
          container.data('pagination', {});
        }

        if (self.callHook('beforeInit') === false) return;

        // Pagination has been initialized, destroy it
        if (container.data('pagination').initialized) {
          $('.paginationjs', container).remove();
        }

        // Whether to disable Pagination at the initialization
        self.disabled = !!attributes.disabled;

        // Model will be passed to the callback function
        var model = self.model = {
          pageRange: attributes.pageRange,
          pageSize: attributes.pageSize
        };

        // Parse dataSource to find available paging data
        self.parseDataSource(attributes.dataSource, function(dataSource) {

          // Asynchronous mode
          self.isAsync = Helpers.isString(dataSource);
          if (Helpers.isArray(dataSource)) {
            model.totalNumber = attributes.totalNumber = dataSource.length;
          }

          // Asynchronous mode and a 'totalNumberLocator' has been specified
          self.isDynamicTotalNumber = self.isAsync && attributes.totalNumberLocator;

          var el = self.render(true);

          // Add extra className to the pagination element
            if (attributes.className) {
            el.addClass(attributes.className);
          }

          model.el = el;

          // Append / prepend pagination element to the container
          container[attributes.position === 'bottom' ? 'append' : 'prepend'](el);

          // Bind events
          self.observer();

          // Mark pagination has been initialized
          container.data('pagination').initialized = true;

          // Call hook after initialization
          self.callHook('afterInit', el);
        });
      },

      render: function(isBoot) {
        var self = this;
        var model = self.model;
        var el = model.el || $('<div class="paginationjs"></div>');
        var isForced = isBoot !== true;

        self.callHook('beforeRender', isForced);

        var currentPage = model.pageNumber || attributes.pageNumber;
        var pageRange = attributes.pageRange || 0;
        var totalPage = self.getTotalPage();

        var rangeStart = currentPage - pageRange;
        var rangeEnd = currentPage + pageRange;

        if (rangeEnd > totalPage) {
          rangeEnd = totalPage;
          rangeStart = totalPage - pageRange * 2;
          rangeStart = rangeStart < 1 ? 1 : rangeStart;
        }

        if (rangeStart <= 1) {
          rangeStart = 1;
          rangeEnd = Math.min(pageRange * 2 + 1, totalPage);
        }

        el.html(self.generateHTML({
          currentPage: currentPage,
          pageRange: pageRange,
          rangeStart: rangeStart,
          rangeEnd: rangeEnd
        }));

        // Whether to hide pagination when there is only one page
        if (attributes.hideOnlyOnePage) {
          el[totalPage <= 1 ? 'hide' : 'show']();
        }

        self.callHook('afterRender', isForced);

        return el;
      },

      getPageLinkTag: function(index) {
        var pageLink = attributes.pageLink;
        return pageLink ? `<a href="${pageLink}">${index}</a>` : `<a>${index}</a>`;
      },

      // Generate HTML for page numbers
      generatePageNumbersHTML: function(args) {
        var self = this;
        var currentPage = args.currentPage;
        var totalPage = self.getTotalPage();
        var getPageLinkTag = self.getPageLinkTag;
        var rangeStart = args.rangeStart;
        var rangeEnd = args.rangeEnd;
        var html = '';
        var i;

        var ellipsisText = attributes.ellipsisText;

        var classPrefix = attributes.classPrefix;
        var pageClassName = attributes.pageClassName || '';
        var activeClassName = attributes.activeClassName || '';
        var disableClassName = attributes.disableClassName || '';

        // Display all page numbers if page range disabled
        if (attributes.pageRange === null) {
          for (i = 1; i <= totalPage; i++) {
            if (i == currentPage) {
              html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName} ${activeClassName}" data-num="${i}"><a>${i}</a></li>`;
            } else {
              html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName}" data-num="${i}">${getPageLinkTag(i)}</li>`;
            }
          }
          return html;
        }

        if (rangeStart <= 3) {
          for (i = 1; i < rangeStart; i++) {
            if (i == currentPage) {
              html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName} ${activeClassName}" data-num="${i}"><a>${i}</a></li>`;
            } else {
              html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName}" data-num="${i}">${getPageLinkTag(i)}</li>`;
            }
          }
        } else {
          if (!attributes.hideFirstOnEllipsisShow) {
            html += `<li class="${classPrefix}-page ${classPrefix}-first J-paginationjs-page ${pageClassName}" data-num="1">${getPageLinkTag(1)}</li>`;
          }
          html += `<li class="${classPrefix}-ellipsis ${disableClassName}"><a>${ellipsisText}</a></li>`;
        }

        for (i = rangeStart; i <= rangeEnd; i++) {
          if (i == currentPage) {
            html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName} ${activeClassName}" data-num="${i}"><a>${i}</a></li>`;
          } else {
            html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName}" data-num="${i}">${getPageLinkTag(i)}</li>`;
          }
        }

        if (rangeEnd >= totalPage - 2) {
          for (i = rangeEnd + 1; i <= totalPage; i++) {
            html += `<li class="${classPrefix}-page J-paginationjs-page ${pageClassName}" data-num="${i}">${getPageLinkTag(i)}</li>`;
          }
        } else {
          html += `<li class="${classPrefix}-ellipsis ${disableClassName}"><a>${ellipsisText}</a></li>`;

          if (!attributes.hideLastOnEllipsisShow) {
            html += `<li class="${classPrefix}-page ${classPrefix}-last J-paginationjs-page ${pageClassName}" data-num="${totalPage}">${getPageLinkTag(totalPage)}</li>`;
          }
        }

        return html;
      },

      // Generate HTML content
      generateHTML: function(args) {
        var self = this;
        var currentPage = args.currentPage;
        var totalPage = self.getTotalPage();
        var getPageLinkTag = self.getPageLinkTag;

        var totalNumber = self.getTotalNumber();

        var pageSize = attributes.pageSize;
        var showPrevious = attributes.showPrevious;
        var showNext = attributes.showNext;
        var showPageNumbers = attributes.showPageNumbers;
        var showNavigator = attributes.showNavigator;
        var showSizeChanger = attributes.showSizeChanger;
        var sizeChangerOptions = attributes.sizeChangerOptions;
        var showGoInput = attributes.showGoInput;
        var showGoButton = attributes.showGoButton;

        var prevText = attributes.prevText;
        var nextText = attributes.nextText;
        var goButtonText = attributes.goButtonText;

        var classPrefix = attributes.classPrefix;
        var disableClassName = attributes.disableClassName || '';
        var ulClassName = attributes.ulClassName || '';
        var prevClassName = attributes.prevClassName || '';
        var nextClassName = attributes.nextClassName || '';

        var html = '';
        var sizeSelect = `<select class="J-paginationjs-size-select">`;
        var goInput = '<input type="text" class="J-paginationjs-go-pagenumber">';
        var goButton = `<input type="button" class="J-paginationjs-go-button" value="${goButtonText}">`;
        var formattedString;

        var formatSizeChanger = typeof attributes.formatSizeChanger === 'function' ? attributes.formatSizeChanger(currentPage, totalPage, totalNumber) : attributes.formatSizeChanger;
        var formatNavigator = typeof attributes.formatNavigator === 'function' ? attributes.formatNavigator(currentPage, totalPage, totalNumber) : attributes.formatNavigator;
        var formatGoInput = typeof attributes.formatGoInput === 'function' ? attributes.formatGoInput(goInput, currentPage, totalPage, totalNumber) : attributes.formatGoInput;
        var formatGoButton = typeof attributes.formatGoButton === 'function' ? attributes.formatGoButton(goButton, currentPage, totalPage, totalNumber) : attributes.formatGoButton;

        var autoHidePrevious = typeof attributes.autoHidePrevious === 'function' ? attributes.autoHidePrevious() : attributes.autoHidePrevious;
        var autoHideNext = typeof attributes.autoHideNext === 'function' ? attributes.autoHideNext() : attributes.autoHideNext;

        var header = typeof attributes.header === 'function' ? attributes.header(currentPage, totalPage, totalNumber) : attributes.header;
        var footer = typeof attributes.footer === 'function' ? attributes.footer(currentPage, totalPage, totalNumber) : attributes.footer;

        // Prepend extra contents to the pagination buttons
        if (header) {
          formattedString = self.replaceVariables(header, {
            currentPage: currentPage,
            totalPage: totalPage,
            totalNumber: totalNumber
          });
          html += formattedString;
        }

        // Whether to display navigator
        if (showNavigator) {
          if (formatNavigator) {
            formattedString = self.replaceVariables(formatNavigator, {
              currentPage: currentPage,
              totalPage: totalPage,
              totalNumber: totalNumber,
              rangeStart: (currentPage - 1) * pageSize + 1,
              rangeEnd: Math.min(currentPage * pageSize, totalNumber)
            });
            html += `<div class="${classPrefix}-nav J-paginationjs-nav">${formattedString}</div>`;
          }
        }

        if (showPrevious || showPageNumbers || showNext) {
          html += '<div class="paginationjs-pages">';

          if (ulClassName) {
            html += `<ul class="${ulClassName}">`;
          } else {
            html += '<ul>';
          }

          // Whether to display Previous button
          if (showPrevious) {
            if (currentPage <= 1) {
              if (!autoHidePrevious) {
                html += `<li class="${classPrefix}-prev ${disableClassName} ${prevClassName}"><a>${prevText}</a></li>`;
              }
            } else {
              html += `<li class="${classPrefix}-prev J-paginationjs-previous ${prevClassName}" data-num="${currentPage - 1}" title="Previous page">${getPageLinkTag(prevText)}</li>`;
            }
          }

          // Whether to display page numbers
          if (showPageNumbers) {
            html += self.generatePageNumbersHTML(args);
          }

          // Whether to display Next button
          if (showNext) {
            if (currentPage >= totalPage) {
              if (!autoHideNext) {
                html += `<li class="${classPrefix}-next ${disableClassName} ${nextClassName}"><a>${nextText}</a></li>`;
              }
            } else {
              html += `<li class="${classPrefix}-next J-paginationjs-next ${nextClassName}" data-num="${currentPage + 1}" title="Next page">${getPageLinkTag(nextText)}</li>`;
            }
          }
          html += `</ul></div>`;
        }

        if (showSizeChanger) {
          if (Helpers.isArray(sizeChangerOptions)) {
            if (sizeChangerOptions.indexOf(pageSize) === -1) {
              sizeChangerOptions.unshift(pageSize);
              sizeChangerOptions.sort((a, b) => a - b);
            }
            for (let i = 0; i < sizeChangerOptions.length; i++) {
              sizeSelect += `<option value="${sizeChangerOptions[i]}"${(sizeChangerOptions[i] === pageSize ? ' selected' : '')}>${sizeChangerOptions[i]} / page</option>`;
            }
            sizeSelect += `</select>`;
            formattedString = sizeSelect;

            if (formatSizeChanger) {
              formattedString = self.replaceVariables(formatSizeChanger, {
                length: sizeSelect,
                total: totalNumber
              });
            }
            html += `<div class="paginationjs-size-changer">${formattedString}</div>`;
          }
        }

        // Whether to display Go input
        if (showGoInput) {
          if (formatGoInput) {
            formattedString = self.replaceVariables(formatGoInput, {
              currentPage: currentPage,
              totalPage: totalPage,
              totalNumber: totalNumber,
              input: goInput
            });
            html += `<div class="${classPrefix}-go-input">${formattedString}</div>`;
          }
        }

        // Whether to display Go button
        if (showGoButton) {
          if (formatGoButton) {
            formattedString = self.replaceVariables(formatGoButton, {
              currentPage: currentPage,
              totalPage: totalPage,
              totalNumber: totalNumber,
              button: goButton
            });
            html += `<div class="${classPrefix}-go-button">${formattedString}</div>`;
          }
        }

        // Append extra contents to the pagination buttons
        if (footer) {
          formattedString = self.replaceVariables(footer, {
            currentPage: currentPage,
            totalPage: totalPage,
            totalNumber: totalNumber
          });
          html += formattedString;
        }

        return html;
      },

      // dataSource is a request URL and a 'totalNumberLocator' function specified
      // execute it to find out 'totalNumber' from the response
      findTotalNumberFromRemoteResponse: function(response) {
        var self = this;
        self.model.totalNumber = attributes.totalNumberLocator(response);
      },

      // Go to the specified page
      go: function(number, callback) {
        var self = this;
        var model = self.model;

        if (self.disabled) return;

        var pageNumber = number;
        pageNumber = parseInt(pageNumber);

        if (!pageNumber || pageNumber < 1) return;

        var pageSize = attributes.pageSize;
        var totalNumber = self.getTotalNumber();
        var totalPage = self.getTotalPage();

        if (totalNumber > 0 && pageNumber > totalPage) return;

        // Pick paging data in synchronous mode
        if (!self.isAsync) {
          render(self.getPagingData(pageNumber));
          return;
        }

        var postData = {};
        var alias = attributes.alias || {};
        var pageSizeName = alias.pageSize ? alias.pageSize : 'pageSize';
        var pageNumberName = alias.pageNumber ? alias.pageNumber : 'pageNumber';
        postData[pageSizeName] = pageSize;
        postData[pageNumberName] = pageNumber;

        var ajaxParams = typeof attributes.ajax === 'function' ? attributes.ajax() : attributes.ajax;

        // If the pageNumber's value starts with 0 via Ajax
        if (ajaxParams && ajaxParams.pageNumberStartWithZero) {
          postData[pageNumberName] = pageNumber - 1;
          delete ajaxParams.pageNumberStartWithZero;
        }

        var formatAjaxParams = {
          type: 'get',
          cache: false,
          data: {},
          contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
          dataType: 'json',
          async: true
        };

        $.extend(true, formatAjaxParams, ajaxParams);
        $.extend(formatAjaxParams.data, postData);

        formatAjaxParams.url = attributes.dataSource;
        formatAjaxParams.success = function(response) {
          self.model.originalResponse = response;
          if (self.isDynamicTotalNumber) {
            self.findTotalNumberFromRemoteResponse(response);
          } else {
            self.model.totalNumber = attributes.totalNumber;
          }

          var finalData = self.filterDataWithLocator(response);
          render(finalData);
        };
        formatAjaxParams.error = function(jqXHR, textStatus, errorThrown) {
          attributes.formatAjaxError && attributes.formatAjaxError(jqXHR, textStatus, errorThrown);
          self.enable();
        };

        self.disable();

        if (attributes.ajaxFunction) {
          attributes.ajaxFunction(formatAjaxParams);
        } else {
          $.ajax(formatAjaxParams);
        }

        function render(data) {
          if (self.callHook('beforePaging', pageNumber) === false) return false;

          // Pagination direction
          model.direction = typeof model.pageNumber === 'undefined' ? 0 : (pageNumber > model.pageNumber ? 1 : -1);

          model.pageNumber = pageNumber;

          self.render();

          if (self.disabled && self.isAsync) {
            // enable pagination
            self.enable();
          }

          // cache model data
          container.data('pagination').model = model;

          // format result data before callback invoked
          if (attributes.formatResult) {
            var cloneData = $.extend(true, [], data);
            if (!Helpers.isArray(data = attributes.formatResult(cloneData))) {
              data = cloneData;
            }
          }

          container.data('pagination').currentPageData = data;

          self.doCallback(data, callback);

          self.callHook('afterPaging', pageNumber);

          if (pageNumber == 1) {
            self.callHook('afterIsFirstPage');
          } else if (pageNumber == self.getTotalPage()) {
            self.callHook('afterIsLastPage');
          }
        }
      },

      doCallback: function(data, customCallback) {
        var self = this;
        var model = self.model;

        if (typeof customCallback === 'function') {
          customCallback(data, model);
        } else if (typeof attributes.callback === 'function') {
          attributes.callback(data, model);
        }
      },

      destroy: function() {
        if (this.callHook('beforeDestroy') === false) return;

        this.model.el.remove();
        container.off();

        // Remove style element
        $('#paginationjs-style').remove();

        this.callHook('afterDestroy');
      },

      previous: function(callback) {
        this.go(this.model.pageNumber - 1, callback);
      },

      next: function(callback) {
        this.go(this.model.pageNumber + 1, callback);
      },

      disable: function() {
        var self = this;
        var source = self.isAsync ? 'async' : 'sync';

        if (self.callHook('beforeDisable', source) === false) return;

        self.disabled = true;
        self.model.disabled = true;

        self.callHook('afterDisable', source);
      },

      enable: function() {
        var self = this;
        var source = self.isAsync ? 'async' : 'sync';

        if (self.callHook('beforeEnable', source) === false) return;

        self.disabled = false;
        self.model.disabled = false;

        self.callHook('afterEnable', source);
      },

      refresh: function(callback) {
        this.go(this.model.pageNumber, callback);
      },

      show: function() {
        var self = this;

        if (self.model.el.is(':visible')) return;

        self.model.el.show();
      },

      hide: function() {
        var self = this;

        if (!self.model.el.is(':visible')) return;

        self.model.el.hide();
      },

      // Replace variables for template string
      replaceVariables: function(template, variables) {
        var formattedString;

        for (var key in variables) {
          var value = variables[key];
          var regexp = new RegExp('<%=\\s*' + key + '\\s*%>', 'img');

          formattedString = (formattedString || template).replace(regexp, value);
        }

        return formattedString;
      },

      getPagingData: function(number) {
        var pageSize = attributes.pageSize;
        var dataSource = attributes.dataSource;
        var totalNumber = this.getTotalNumber();

        var start = pageSize * (number - 1) + 1;
        var end = Math.min(number * pageSize, totalNumber);

        return dataSource.slice(start - 1, end);
      },

      getTotalNumber: function() {
        return this.model.totalNumber || attributes.totalNumber || 0;
      },

      getTotalPage: function() {
        return Math.ceil(this.getTotalNumber() / attributes.pageSize);
      },

      getLocator: function(locator) {
        var result;

        if (typeof locator === 'string') {
          result = locator;
        } else if (typeof locator === 'function') {
          result = locator();
        } else {
          throwError('"locator" is incorrect. Expect string or function type.');
        }

        return result;
      },

      // Filter data with "locator"
      filterDataWithLocator: function(dataSource) {
        var locator = this.getLocator(attributes.locator);
        var filteredData;

        // Datasource is an Object, use "locator" to locate available data
        if (Helpers.isObject(dataSource)) {
          try {
            $.each(locator.split('.'), function(index, item) {
              filteredData = (filteredData ? filteredData : dataSource)[item];
            });
          }
          catch (e) {
            // ignore
          }

          if (!filteredData) {
            throwError('dataSource.' + locator + ' is undefined.');
          } else if (!Helpers.isArray(filteredData)) {
            throwError('dataSource.' + locator + ' should be an Array.');
          }
        }

        return filteredData || dataSource;
      },

      parseDataSource: function(dataSource, callback) {
        var self = this;

        if (Helpers.isObject(dataSource)) {
          callback(attributes.dataSource = self.filterDataWithLocator(dataSource));
        } else if (Helpers.isArray(dataSource)) {
          callback(attributes.dataSource = dataSource);
        } else if (typeof dataSource === 'function') {
          attributes.dataSource(function(data) {
            if (!Helpers.isArray(data)) {
              throwError('The parameter of "done" Function should be an Array.');
            }
            self.parseDataSource.call(self, data, callback);
          });
        } else if (typeof dataSource === 'string') {
          if (/^https?|file:/.test(dataSource)) {
            attributes.ajaxDataType = 'jsonp';
          }
          callback(dataSource);
        } else {
          throwError('Unexpected dataSource type');
        }
      },

      callHook: function(hook) {
        var paginationData = container.data('pagination') || {};
        var result;

        var args = Array.prototype.slice.apply(arguments);
        args.shift();

        if (attributes[hook] && typeof attributes[hook] === 'function') {
          if (attributes[hook].apply(global, args) === false) {
            result = false;
          }
        }

        if (paginationData.hooks && paginationData.hooks[hook]) {
          $.each(paginationData.hooks[hook], function(index, item) {
            if (item.apply(global, args) === false) {
              result = false;
            }
          });
        }

        return result !== false;
      },

      observer: function() {
        var self = this;
        var el = self.model.el;

        // Go to specified page number
        container.on(eventPrefix + 'go', function(event, pageNumber, done) {
          if (typeof pageNumber === 'string') {
            pageNumber = parseInt(pageNumber.trim());
          }

          if (!pageNumber) return;

          if (typeof pageNumber !== 'number') {
            throwError('"pageNumber" is incorrect. (Number)');
          }

          self.go(pageNumber, done);
        });

        // Page number button click listener
        el.on('click', '.J-paginationjs-page', function(event) {
          var current = $(event.currentTarget);
          var pageNumber = current.attr('data-num').trim();

          if (!pageNumber || current.hasClass(attributes.disableClassName) || current.hasClass(attributes.activeClassName)) return;

          if (self.callHook('beforePageOnClick', event, pageNumber) === false) return false;

          self.go(pageNumber);

          self.callHook('afterPageOnClick', event, pageNumber);

          if (!attributes.pageLink) return false;
        });

        // Previous button click listener
        el.on('click', '.J-paginationjs-previous', function(event) {
          var current = $(event.currentTarget);
          var pageNumber = current.attr('data-num').trim();

          if (!pageNumber || current.hasClass(attributes.disableClassName)) return;

          if (self.callHook('beforePreviousOnClick', event, pageNumber) === false) return false;

          self.go(pageNumber);

          self.callHook('afterPreviousOnClick', event, pageNumber);

          if (!attributes.pageLink) return false;
        });

        // Next button click listener
        el.on('click', '.J-paginationjs-next', function(event) {
          var current = $(event.currentTarget);
          var pageNumber = current.attr('data-num').trim();

          if (!pageNumber || current.hasClass(attributes.disableClassName)) return;

          if (self.callHook('beforeNextOnClick', event, pageNumber) === false) return false;

          self.go(pageNumber);

          self.callHook('afterNextOnClick', event, pageNumber);

          if (!attributes.pageLink) return false;
        });

        // Go button click listener
        el.on('click', '.J-paginationjs-go-button', function(event) {
          var pageNumber = $('.J-paginationjs-go-pagenumber', el).val();

          if (self.callHook('beforeGoButtonOnClick', event, pageNumber) === false) return false;

          container.trigger(eventPrefix + 'go', pageNumber);

          self.callHook('afterGoButtonOnClick', event, pageNumber);
        });

        // go input enter keyup listener
        el.on('keyup', '.J-paginationjs-go-pagenumber', function(event) {
          if (event.which === 13) {
            var pageNumber = $(event.currentTarget).val();

            if (self.callHook('beforeGoInputOnEnter', event, pageNumber) === false) return false;

            container.trigger(eventPrefix + 'go', pageNumber);

            // Maintain the cursor
            $('.J-paginationjs-go-pagenumber', el).focus();

            self.callHook('afterGoInputOnEnter', event, pageNumber);
          }
        });

        el.on('change', '.J-paginationjs-size-select', function(event) {
          var current = $(event.currentTarget);
          var size = parseInt(current.val());
          var currentPage = self.model.pageNumber || attributes.pageNumber;

          if (typeof size !== 'number') return;

          if (self.callHook('beforeSizeSelectorChange', event, size) === false) return false;

          attributes.pageSize = size;
          self.model.pageSize = size;
          self.model.totalPage = self.getTotalPage();
          if (currentPage > self.model.totalPage) {
            currentPage = self.model.totalPage;
          }
          self.go(currentPage);

          self.callHook('afterSizeSelectorChange', event, size);

          if (!attributes.pageLink) return false;
        });

        // Previous page
        container.on(eventPrefix + 'previous', function(event, done) {
          self.previous(done);
        });

        // Next page
        container.on(eventPrefix + 'next', function(event, done) {
          self.next(done);
        });

        // Disable
        container.on(eventPrefix + 'disable', function() {
          self.disable();
        });

        // Enable
        container.on(eventPrefix + 'enable', function() {
          self.enable();
        });

        // Refresh
        container.on(eventPrefix + 'refresh', function(event, done) {
          self.refresh(done);
        });

        // Show
        container.on(eventPrefix + 'show', function() {
          self.show();
        });

        // Hide
        container.on(eventPrefix + 'hide', function() {
          self.hide();
        });

        // Destroy
        container.on(eventPrefix + 'destroy', function() {
          self.destroy();
        });

        // Whether to load the default page
        var validTotalPage = Math.max(self.getTotalPage(), 1)
        var defaultPageNumber = attributes.pageNumber;
        
        // Default pageNumber should be 1 when totalNumber is dynamic
        if (self.isDynamicTotalNumber) {
          if (attributes.resetPageNumberOnInit) defaultPageNumber = 1;
        }

        if (attributes.triggerPagingOnInit) {
          container.trigger(eventPrefix + 'go', Math.min(defaultPageNumber, validTotalPage));
        }
      }
    };

    // Pagination has been initialized
    if (container.data('pagination') && container.data('pagination').initialized === true) {
      // Handle events
      if (isNumeric(options)) {
        // eg: container.pagination(5)
        container.trigger.call(this, eventPrefix + 'go', options, arguments[1]);
        return this;
      } else if (typeof options === 'string') {
        var args = Array.prototype.slice.apply(arguments);
          args[0] = eventPrefix + args[0];

        switch (options) {
          case 'previous':
          case 'next':
          case 'go':
          case 'disable':
          case 'enable':
          case 'refresh':
          case 'show':
          case 'hide':
          case 'destroy':
            container.trigger.apply(this, args);
            break;
          case 'getSelectedPageNum':
          case 'getCurrentPageNum':
            if (container.data('pagination').model) {
              return container.data('pagination').model.pageNumber;
            } else {
              return container.data('pagination').attributes.pageNumber;
            }
          case 'getTotalPage':
            return Math.ceil(container.data('pagination').model.totalNumber / container.data('pagination').model.pageSize);
          case 'getSelectedPageData':
          case 'getCurrentPageData':
            return container.data('pagination').currentPageData;
          // Whether pagination has been disabled
          case 'isDisabled':
            return container.data('pagination').model.disabled === true;
          default:
            throwError('Unknown action: ' + options);
        }
        return this;
      } else {
        // Uninstall the old instance before initializing a new one
        uninstallPlugin(container);
      }
    } else {
      if (!Helpers.isObject(options)) throwError('Illegal options');
    }

    // Check parameters
    parameterChecker(attributes);

    pagination.initialize();

    return this;
  };

  // Instance defaults
  $.fn[pluginName].defaults = {

    // Data source
    // Array | String | Function | Object
    //dataSource: '',

    // String | Function
    //locator: 'data',

    // Function
    //totalNumberLocator: function() {},

    // Total number of data items
    totalNumber: 0,

    // Default page number
    pageNumber: 1,

    // Number of data items per page
    pageSize: 10,

    // Page range (pages around current page)
    pageRange: 2,

    // Whether to display the 'Previous' button
    showPrevious: true,

    // Whether to display the 'Next' button
    showNext: true,

    // Whether to display the page buttons
    showPageNumbers: true,

    showNavigator: false,

    // Whether to display the 'Go' input
    showGoInput: false,

    // Whether to display the 'Go' button
    showGoButton: false,

    showSizeChanger: false,

    sizeChangerOptions: [10, 20, 50, 100],

    // Page link
    pageLink: '',

    // 'Previous' text
    prevText: '&lsaquo;',

    // 'Next' text
    nextText: '&rsaquo;',

    // Ellipsis text
    ellipsisText: '...',

    // 'Go' button text
    goButtonText: 'Go',

    // Additional class name(s) for the Pagination container
    //className: '',

    classPrefix: 'paginationjs',

    activeClassName: 'active',

    // class name when disabled
    disableClassName: 'disabled',

    //ulClassName: '',

    //pageClassName: '',

    //prevClassName: '',

    //nextClassName: '',

    formatNavigator: 'Total <%= totalNumber %> items',

    formatGoInput: '<%= input %>',

    formatGoButton: '<%= button %>',

    // position in the container
    position: 'bottom',

    // Auto hide previous button when current page is the first
    autoHidePrevious: false,

    // Auto hide next button when current page is the last
    autoHideNext: false,

    //header: '',

    //footer: '',

    //alias: {},

    // Whether to trigger pagination at initialization
    triggerPagingOnInit: true,

    // Whether to reset page number at initialization, it works only if dataSource is a URL and totalNumberLocator is specified
    resetPageNumberOnInit: true,

    // Whether to hide pagination when less than one page
    hideOnlyOnePage: false,

    hideFirstOnEllipsisShow: false,

    hideLastOnEllipsisShow: false,

    // Customize item's innerHTML
    callback: function() {}
  };

  // Hook register
  $.fn[pluginHookMethod] = function(hook, callback) {
    if (arguments.length < 2) {
      throwError('Expect 2 arguments at least.');
    }

    if (typeof callback !== 'function') {
      throwError('callback should be a function.');
    }

    var container = $(this);
    var paginationData = container.data('pagination');

    if (!paginationData) {
      container.data('pagination', {});
      paginationData = container.data('pagination');
    }

    !paginationData.hooks && (paginationData.hooks = {});

    //paginationData.hooks[hook] = callback;
    paginationData.hooks[hook] = paginationData.hooks[hook] || [];
    paginationData.hooks[hook].push(callback);

  };

  // Static method
  $[pluginName] = function(selector, options) {
    if (arguments.length < 2) {
      throwError('Requires two parameters.');
    }

    var container;

    // 'selector' is a jQuery object
    if (typeof selector !== 'string' && selector instanceof jQuery) {
      container = selector;
    } else {
      container = $(selector);
    }

    if (!container.length) return;

    container.pagination(options);

    return container;
  };

  // ============================================================
  // helpers
  // ============================================================

  var Helpers = {};

  // Throw error
  function throwError(content) {
    throw new Error('Pagination: ' + content);
  }

  // Check parameters
  function parameterChecker(args) {
    if (!args.dataSource) {
      throwError('"dataSource" is required.');
    }

    if (typeof args.dataSource === 'string') {
      if (args.totalNumberLocator === undefined) {
        if (args.totalNumber === undefined) {
          throwError('"totalNumber" is required.');
        } else if (!isNumeric(args.totalNumber)) {
          throwError('"totalNumber" is incorrect. Expect numberic type');
        }
      } else {
        if (typeof args.totalNumberLocator !== 'function') {
          throwError('"totalNumberLocator" should be a Function.');
        }
      }
    } else if (Helpers.isObject(args.dataSource)) {
      if (typeof args.locator === 'undefined') {
        throwError('"dataSource" is an Object, please specify a "locator".');
      } else if (typeof args.locator !== 'string' && typeof args.locator !== 'function') {
        throwError('' + args.locator + ' is incorrect. Expect string or function type');
      }
    }

    if (args.formatResult !== undefined && typeof args.formatResult !== 'function') {
      throwError('"formatResult" should be a Function.');
    }
  }

  // uninstall plugin
  function uninstallPlugin(target) {
    var events = ['go', 'previous', 'next', 'disable', 'enable', 'refresh', 'show', 'hide', 'destroy'];

    // off all events
    $.each(events, function(index, value) {
      target.off(eventPrefix + value);
    });

    // reset pagination data
    target.data('pagination', {});

    // remove pagination element
    $('.paginationjs', target).remove();
  }

  // Object type detection
  function getObjectType(object, tmp) {
    return ( (tmp = typeof(object)) == "object" ? object == null && "null" || Object.prototype.toString.call(object).slice(8, -1) : tmp ).toLowerCase();
  }

  function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
  }

  $.each(['Object', 'Array', 'String'], function(index, name) {
    Helpers['is' + name] = function(object) {
      return getObjectType(object) === name.toLowerCase();
    };
  });

  /*
   * export via AMD or CommonJS
   * */
  if (typeof define === 'function' && define.amd) {
    define(function() {
      return $;
    });
  }

})(this, window.jQuery);
