/**

* dirPagination - AngularJS module for paginating (almost) anything.
*
*
* Credits
* =======
*
* Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ
* for the idea on how to dynamically invoke the ng-repeat directive.
*
* I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project:
* https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js
*
* Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk>
*/

(function() {

/**
 * Config
 */
var moduleName = 'angularUtils.directives.dirPagination';
var DEFAULT_ID = '__default';

/**
 * Module
 */
var module;
try {
    module = angular.module(moduleName);
} catch(err) {
    // named module does not exist, so create one
    module = angular.module(moduleName, []);
}

module
    .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
    .directive('dirPaginateNoCompile', noCompileDirective)
    .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
    .filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
    .service('paginationService', paginationService)
    .provider('paginationTemplate', paginationTemplateProvider);

function dirPaginateDirective($compile, $parse, paginationService) {

    return  {
        terminal: true,
        multiElement: true,
        compile: dirPaginationCompileFn
    };

    function dirPaginationCompileFn(tElement, tAttrs){

        var expression = tAttrs.dirPaginate;
        // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211
        var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);

        var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/;
        if (match[2].match(filterPattern) === null) {
            throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
        }
        var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
        var collectionGetter = $parse(itemsPerPageFilterRemoved);

        addNoCompileAttributes(tElement);

        // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
        // dir-pagination-controls directives that may be looking for this ID.
        var rawId = tAttrs.paginationId || DEFAULT_ID;
        paginationService.registerInstance(rawId);

        return function dirPaginationLinkFn(scope, element, attrs){

            // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
            // potentially register a new ID if it evaluates to a different value than the rawId.
            var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
            paginationService.registerInstance(paginationId);

            var repeatExpression = getRepeatExpression(expression, paginationId);
            addNgRepeatToElement(element, attrs, repeatExpression);

            removeTemporaryAttributes(element);
            var compiled =  $compile(element);

            var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
            paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);

            if (typeof attrs.totalItems !== 'undefined') {
                paginationService.setAsyncModeTrue(paginationId);
                scope.$watch(function() {
                    return $parse(attrs.totalItems)(scope);
                }, function (result) {
                    if (0 <= result) {
                        paginationService.setCollectionLength(paginationId, result);
                    }
                });
            } else {
                scope.$watchCollection(function() {
                    return collectionGetter(scope);
                }, function(collection) {
                    if (collection) {
                        paginationService.setCollectionLength(paginationId, collection.length);
                    }
                });
            }

            // Delegate to the link function returned by the new compilation of the ng-repeat
            compiled(scope);
        };
    }

    /**
     * If a pagination id has been specified, we need to check that it is present as the second argument passed to
     * the itemsPerPage filter. If it is not there, we add it and return the modified expression.
     *
     * @param expression
     * @param paginationId
     * @returns {*}
     */
    function getRepeatExpression(expression, paginationId) {
        var repeatExpression,
            idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);

        if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
            repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'");
        } else {
            repeatExpression = expression;
        }

        return repeatExpression;
    }

    /**
     * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
     * appropriate multi-element ng-repeat to the first and last element in the range.
     * @param element
     * @param attrs
     * @param repeatExpression
     */
    function addNgRepeatToElement(element, attrs, repeatExpression) {
        if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
            // using multiElement mode (dir-paginate-start, dir-paginate-end)
            attrs.$set('ngRepeatStart', repeatExpression);
            element.eq(element.length - 1).attr('ng-repeat-end', true);
        } else {
            attrs.$set('ngRepeat', repeatExpression);
        }
    }

    /**
     * Adds the dir-paginate-no-compile directive to each element in the tElement range.
     * @param tElement
     */
    function addNoCompileAttributes(tElement) {
        angular.forEach(tElement, function(el) {
            if (el.nodeType === Node.ELEMENT_NODE) {
                angular.element(el).attr('dir-paginate-no-compile', true);
            }
        });
    }

    /**
     * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
     * @param element
     */
    function removeTemporaryAttributes(element) {
        angular.forEach(element, function(el) {
            if (el.nodeType === Node.ELEMENT_NODE) {
                angular.element(el).removeAttr('dir-paginate-no-compile');
            }
        });
        element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
        element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
    }

    /**
     * Creates a getter function for the current-page attribute, using the expression provided or a default value if
     * no current-page expression was specified.
     *
     * @param scope
     * @param attrs
     * @param paginationId
     * @returns {*}
     */
    function makeCurrentPageGetterFn(scope, attrs, paginationId) {
        var currentPageGetter;
        if (attrs.currentPage) {
            currentPageGetter = $parse(attrs.currentPage);
        } else {
            // if the current-page attribute was not set, we'll make our own
            var defaultCurrentPage = paginationId + '__currentPage';
            scope[defaultCurrentPage] = 1;
            currentPageGetter = $parse(defaultCurrentPage);
        }
        return currentPageGetter;
    }
}

/**
 * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
 * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
 * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
 */
function noCompileDirective() {
    return {
        priority: 5000,
        terminal: true
    };
}

function dirPaginationControlsDirective(paginationService, paginationTemplate) {

    var numberRegex = /^\d+$/;

    return {
        restrict: 'AE',
        templateUrl: function(elem, attrs) {
            return attrs.templateUrl || paginationTemplate.getPath();
        },
        scope: {
            maxSize: '=?',
            onPageChange: '&?',
            paginationId: '=?'
        },
        link: dirPaginationControlsLinkFn
    };

    function dirPaginationControlsLinkFn(scope, element, attrs) {

        // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
        // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is
        // no corresponding dir-paginate directive and wrongly throwing an exception.
        var rawId = attrs.paginationId ||  DEFAULT_ID;
        var paginationId = scope.paginationId || attrs.paginationId ||  DEFAULT_ID;

        if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
            var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
            throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.';
        }

        if (!scope.maxSize) { scope.maxSize = 9; }
        scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
        scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;

        var paginationRange = Math.max(scope.maxSize, 5);
        scope.pages = [];
        scope.pagination = {
            last: 1,
            current: 1
        };
        scope.range = {
            lower: 1,
            upper: 1,
            total: 1
        };

        scope.$watch(function() {
            return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
        }, function(length) {
            if (0 < length) {
                generatePagination();
            }
        });

        scope.$watch(function() {
            return (paginationService.getItemsPerPage(paginationId));
        }, function(current, previous) {
            if (current != previous && typeof previous !== 'undefined') {
                goToPage(scope.pagination.current);
            }
        });

        scope.$watch(function() {
            return paginationService.getCurrentPage(paginationId);
        }, function(currentPage, previousPage) {
            if (currentPage != previousPage) {
                goToPage(currentPage);
            }
        });

        scope.setCurrent = function(num) {
            if (isValidPageNumber(num)) {
                num = parseInt(num, 10);
                paginationService.setCurrentPage(paginationId, num);
            }
        };

        function goToPage(num) {
            if (isValidPageNumber(num)) {
                scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
                scope.pagination.current = num;
                updateRangeValues();

                // if a callback has been set, then call it with the page number as an argument
                if (scope.onPageChange) {
                    scope.onPageChange({ newPageNumber : num });
                }
            }
        }

        function generatePagination() {
            var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;

            scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
            scope.pagination.current = page;
            scope.pagination.last = scope.pages[scope.pages.length - 1];
            if (scope.pagination.last < scope.pagination.current) {
                scope.setCurrent(scope.pagination.last);
            } else {
                updateRangeValues();
            }
        }

        /**
         * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
         * template to display the current page range, e.g. "showing 21 - 40 of 144 results";
         */
        function updateRangeValues() {
            var currentPage = paginationService.getCurrentPage(paginationId),
                itemsPerPage = paginationService.getItemsPerPage(paginationId),
                totalItems = paginationService.getCollectionLength(paginationId);

            scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
            scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
            scope.range.total = totalItems;
        }

        function isValidPageNumber(num) {
            return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
        }
    }

    /**
     * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
     * links used in pagination
     *
     * @param currentPage
     * @param rowsPerPage
     * @param paginationRange
     * @param collectionLength
     * @returns {Array}
     */
    function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) {
        var pages = [];
        var totalPages = Math.ceil(collectionLength / rowsPerPage);
        var halfWay = Math.ceil(paginationRange / 2);
        var position;

        if (currentPage <= halfWay) {
            position = 'start';
        } else if (totalPages - halfWay < currentPage) {
            position = 'end';
        } else {
            position = 'middle';
        }

        var ellipsesNeeded = paginationRange < totalPages;
        var i = 1;
        while (i <= totalPages && i <= paginationRange) {
            var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);

            var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end'));
            var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start'));
            if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
                pages.push('...');
            } else {
                pages.push(pageNumber);
            }
            i ++;
        }
        return pages;
    }

    /**
     * Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position.
     *
     * @param i
     * @param currentPage
     * @param paginationRange
     * @param totalPages
     * @returns {*}
     */
    function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
        var halfWay = Math.ceil(paginationRange/2);
        if (i === paginationRange) {
            return totalPages;
        } else if (i === 1) {
            return i;
        } else if (paginationRange < totalPages) {
            if (totalPages - halfWay < currentPage) {
                return totalPages - paginationRange + i;
            } else if (halfWay < currentPage) {
                return currentPage - halfWay + i;
            } else {
                return i;
            }
        } else {
            return i;
        }
    }
}

/**
 * This filter slices the collection into pages based on the current page number and number of items per page.
 * @param paginationService
 * @returns {Function}
 */
function itemsPerPageFilter(paginationService) {

    return function(collection, itemsPerPage, paginationId) {
        if (typeof (paginationId) === 'undefined') {
            paginationId = DEFAULT_ID;
        }
        if (!paginationService.isRegistered(paginationId)) {
            throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
        }
        var end;
        var start;
        if (collection instanceof Array) {
            itemsPerPage = parseInt(itemsPerPage) || 9999999999;
            if (paginationService.isAsyncMode(paginationId)) {
                start = 0;
            } else {
                start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage;
            }
            end = start + itemsPerPage;
            paginationService.setItemsPerPage(paginationId, itemsPerPage);

            return collection.slice(start, end);
        } else {
            return collection;
        }
    };
}

/**
 * This service allows the various parts of the module to communicate and stay in sync.
 */
function paginationService() {

    var instances = {};
    var lastRegisteredInstance;

    this.registerInstance = function(instanceId) {
        if (typeof instances[instanceId] === 'undefined') {
            instances[instanceId] = {
                asyncMode: false
            };
            lastRegisteredInstance = instanceId;
        }
    };

    this.isRegistered = function(instanceId) {
        return (typeof instances[instanceId] !== 'undefined');
    };

    this.getLastInstanceId = function() {
        return lastRegisteredInstance;
    };

    this.setCurrentPageParser = function(instanceId, val, scope) {
        instances[instanceId].currentPageParser = val;
        instances[instanceId].context = scope;
    };
    this.setCurrentPage = function(instanceId, val) {
        instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
    };
    this.getCurrentPage = function(instanceId) {
        var parser = instances[instanceId].currentPageParser;
        return parser ? parser(instances[instanceId].context) : 1;
    };

    this.setItemsPerPage = function(instanceId, val) {
        instances[instanceId].itemsPerPage = val;
    };
    this.getItemsPerPage = function(instanceId) {
        return instances[instanceId].itemsPerPage;
    };

    this.setCollectionLength = function(instanceId, val) {
        instances[instanceId].collectionLength = val;
    };
    this.getCollectionLength = function(instanceId) {
        return instances[instanceId].collectionLength;
    };

    this.setAsyncModeTrue = function(instanceId) {
        instances[instanceId].asyncMode = true;
    };

    this.isAsyncMode = function(instanceId) {
        return instances[instanceId].asyncMode;
    };
}

/**
 * This provider allows global configuration of the template path used by the dir-pagination-controls directive.
 */
function paginationTemplateProvider() {

    var templatePath = 'directives/pagination/dirPagination.tpl.html';

    this.setPath = function(path) {
        templatePath = path;
    };

    this.$get = function() {
        return {
            getPath: function() {
                return templatePath;
            }
        };
    };
}

})();