(function () {
  'use strict';

  /**
   * @ngdoc overview
   * @name ui.grid.selection
   * @description
   *
   * # ui.grid.selection
   * This module provides row selection
   *
   * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
   *
   * <div doc-module-components="ui.grid.selection"></div>
   */

  var module = angular.module('ui.grid.selection', ['ui.grid']);

  /**
   *  @ngdoc object
   *  @name ui.grid.selection.constant:uiGridSelectionConstants
   *
   *  @description constants available in selection module
   */
  module.constant('uiGridSelectionConstants', {
    featureName: 'selection',
    selectionRowHeaderColName: 'selectionRowHeaderCol'
  });

  // add methods to GridRow
  angular.module('ui.grid').config(['$provide', function ($provide) {
    $provide.decorator('GridRow', ['$delegate', function ($delegate) {

      /**
       *  @ngdoc object
       *  @name ui.grid.selection.api:GridRow
       *
       *  @description GridRow prototype functions added for selection
       */

      /**
       *  @ngdoc object
       *  @name enableSelection
       *  @propertyOf  ui.grid.selection.api:GridRow
       *  @description Enable row selection for this row, only settable by internal code.
       *
       *  The grouping feature, for example, might set group header rows to not be selectable.
       *  <br/>Defaults to true
       */

      /**
       *  @ngdoc object
       *  @name isSelected
       *  @propertyOf  ui.grid.selection.api:GridRow
       *  @description Selected state of row. Should be readonly. Make any changes to selected state using setSelected().
       *  <br/>Defaults to false
       */

      /**
       *  @ngdoc object
       *  @name isFocused
       *  @propertyOf  ui.grid.selection.api:GridRow
       *  @description Focused state of row. Should be readonly. Make any changes to focused state using setFocused().
       *  <br/>Defaults to false
       */

      /**
       * @ngdoc function
       * @name setSelected
       * @methodOf ui.grid.selection.api:GridRow
       * @description Sets the isSelected property and updates the selectedCount
       * Changes to isSelected state should only be made via this function
       * @param {Boolean} selected value to set
       */
      $delegate.prototype.setSelected = function (selected) {
        if (selected !== this.isSelected) {
          this.isSelected = selected;
          this.grid.selection.selectedCount += selected ? 1 : -1;
        }
      };

      /**
       * @ngdoc function
       * @name setFocused
       * @methodOf ui.grid.selection.api:GridRow
       * @description Sets the isFocused property
       * Changes to isFocused state should only be made via this function
       * @param {Boolean} val value to set
       */
      $delegate.prototype.setFocused = function(val) {
        if (val !== this.isFocused) {
          this.grid.selection.focusedRow && (this.grid.selection.focusedRow.isFocused = false);
          this.grid.selection.focusedRow = val ? this : null;
          this.isFocused = val;
        }
      };

      return $delegate;
    }]);
  }]);

  /**
   *  @ngdoc service
   *  @name ui.grid.selection.service:uiGridSelectionService
   *
   *  @description Services for selection features
   */
  module.service('uiGridSelectionService',
    function () {
      var service = {

        initializeGrid: function (grid) {

          // add feature namespace and any properties to grid for needed
          /**
           *  @ngdoc object
           *  @name ui.grid.selection.grid:selection
           *
           *  @description Grid properties and functions added for selection
           */
          grid.selection = {
            lastSelectedRow: null,
            /**
             *  @ngdoc object
             *  @name focusedRow
             *  @propertyOf  ui.grid.selection.grid:selection
             *  @description Focused row.
             */
            focusedRow: null,
            selectAll: false
          };


          /**
           *  @ngdoc object
           *  @name selectedCount
           *  @propertyOf  ui.grid.selection.grid:selection
           *  @description Current count of selected rows
           *  @example
           *  var count = grid.selection.selectedCount
           */
          grid.selection.selectedCount = 0;

          service.defaultGridOptions(grid.options);

          /**
           *  @ngdoc object
           *  @name ui.grid.selection.api:PublicApi
           *
           *  @description Public Api for selection feature
           */
          var publicApi = {
            events: {
              selection: {
                /**
                 * @ngdoc event
                 * @name rowFocusChanged
                 * @eventOf  ui.grid.selection.api:PublicApi
                 * @description  is raised after the row.isFocused state is changed
                 * @param {object} scope the scope associated with the grid
                 * @param {GridRow} row the row that was focused/unfocused
                 * @param {Event} evt object if raised from an event
                 */
                rowFocusChanged: function (scope, row, evt) {},
                /**
                 * @ngdoc event
                 * @name rowSelectionChanged
                 * @eventOf  ui.grid.selection.api:PublicApi
                 * @description  is raised after the row.isSelected state is changed
                 * @param {object} scope the scope associated with the grid
                 * @param {GridRow} row the row that was selected/deselected
                 * @param {Event} evt object if raised from an event
                 */
                rowSelectionChanged: function (scope, row, evt) {
                },
                /**
                 * @ngdoc event
                 * @name rowSelectionChangedBatch
                 * @eventOf  ui.grid.selection.api:PublicApi
                 * @description  is raised after the row.isSelected state is changed
                 * in bulk, if the `enableSelectionBatchEvent` option is set to true
                 * (which it is by default).  This allows more efficient processing
                 * of bulk events.
                 * @param {object} scope the scope associated with the grid
                 * @param {array} rows the rows that were selected/deselected
                 * @param {Event} evt object if raised from an event
                 */
                rowSelectionChangedBatch: function (scope, rows, evt) {
                }
              }
            },
            methods: {
              selection: {
                /**
                 * @ngdoc function
                 * @name toggleRowSelection
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Toggles data row as selected or unselected
                 * @param {object} rowEntity gridOptions.data[] array instance
                 * @param {Event} evt object if raised from an event
                 */
                toggleRowSelection: function (rowEntity, evt) {
                  var row = grid.getRow(rowEntity);
                  if (row != void 0 && row !== null) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, true);
                  }
                },
                /**
                 * @ngdoc function
                 * @name selectRow
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Select the data row
                 * @param {object} rowEntity gridOptions.data[] array instance
                 * @param {Event} evt object if raised from an event
                 */
                selectRow: function (rowEntity, evt) {
                  var row = grid.getRow(rowEntity);
                  if (row != void 0 && row !== null && !row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, true);
                  }
                },
                /**
                 * @ngdoc function
                 * @name selectRowByVisibleIndex
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Select the specified row by visible index (i.e. if you
                 * specify row 0 you'll get the first visible row selected).  In this context
                 * visible means of those rows that are theoretically visible (i.e. not filtered),
                 * rather than rows currently rendered on the screen.
                 * @param {number} rowNum index within the rowsVisible array
                 * @param {Event} evt object if raised from an event
                 */
                selectRowByVisibleIndex: function (rowNum, evt) {
                  var row = grid.renderContainers.body.visibleRowCache[rowNum];
                  if (row != void 0 && row !== null && typeof (row) !== 'undefined' && !row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, false);
                  }
                },
                /**
                 * @ngdoc function
                 * @name selectRowByKey
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Select the data row
                 * @param {boolean} isInEntity if true then key is in entity else it's directly in row
                 * @param {Symbol} key the key to look for
                 * @param {any} comparator the value that key should have
                 * @param {Event} evt object if raised from an event
                 * @param {array} lookInRows [optional] the rows to look in - if not provided then looks in grid.rows
                */
                selectRowByKey: function (isInEntity, key, comparator, evt, lookInRows) {
                  var row = grid.findRowByKey(isInEntity, key, comparator, lookInRows);
                  if (row != void 0 && row !== null && typeof (row) !== 'undefined' && !row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, false);
                  }
                },
                /**
                 * @ngdoc function
                 * @name unSelectRow
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description UnSelect the data row
                 * @param {object} rowEntity gridOptions.data[] array instance
                 * @param {Event} evt object if raised from an event
                 */
                unSelectRow: function (rowEntity, evt) {
                  var row = grid.getRow(rowEntity);
                  if (row != void 0 && row !== null && row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, true);
                  }
                },
                /**
                 * @ngdoc function
                 * @name unSelectRowByVisibleIndex
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Unselect the specified row by visible index (i.e. if you
                 * specify row 0 you'll get the first visible row unselected).  In this context
                 * visible means of those rows that are theoretically visible (i.e. not filtered),
                 * rather than rows currently rendered on the screen.
                 * @param {number} rowNum index within the rowsVisible array
                 * @param {Event} evt object if raised from an event
                 */
                unSelectRowByVisibleIndex: function (rowNum, evt) {
                  var row = grid.renderContainers.body.visibleRowCache[rowNum];
                  if (row != void 0 && row !== null && typeof (row) !== 'undefined' &&  row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, false);
                  }
                },
                /**
                 * @ngdoc function
                 * @name unSelectRowByKey
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Select the data row
                 * @param {boolean} isInEntity if true then key is in entity else it's directly in row
                 * @param {(string|number)} key the key to look for
                 * @param {any} comparator the value that key should have
                 * @param {Event} evt object if raised from an event
                 * @param {array} lookInRows [optional] the rows to look in - if not provided then looks in grid.rows
                */
                unSelectRowByKey: function (isInEntity, key, comparator, evt, lookInRows) {
                  var row = grid.findRowByKey(isInEntity, key, comparator, lookInRows);
                  if (row != void 0 && row !== null && typeof (row) !== 'undefined' &&  row.isSelected) {
                    service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect, false);
                  }
                },
                /**
                 * @ngdoc function
                 * @name selectAllRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Selects all rows.  Does nothing if multiSelect = false
                 * @param {Event} evt object if raised from an event
                 */
                selectAllRows: function (evt) {
                  if (grid.options.multiSelect !== false) {
                    var changedRows = [];
                    grid.rows.forEach(function (row) {
                      if (!row.isSelected && row.enableSelection !== false && grid.options.isRowSelectable(row) !== false) {
                        row.setSelected(true);
                        service.decideRaiseSelectionEvent(grid, row, changedRows, evt);
                      }
                    });
                    grid.selection.selectAll = true;
                    service.decideRaiseSelectionBatchEvent(grid, changedRows, evt);
                  }
                },
                /**
                 * @ngdoc function
                 * @name selectAllVisibleRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Selects all visible rows.  Does nothing if multiSelect = false
                 * @param {Event} evt object if raised from an event
                 */
                selectAllVisibleRows: function (evt) {
                  if (grid.options.multiSelect !== false) {
                    var changedRows = [];
                    grid.rows.forEach(function(row) {
                      if (row.visible) {
                        if (!row.isSelected && row.enableSelection !== false && grid.options.isRowSelectable(row) !== false) {
                          row.setSelected(true);
                          service.decideRaiseSelectionEvent(grid, row, changedRows, evt);
                        }
                      } else if (row.isSelected) {
                        row.setSelected(false);
                        service.decideRaiseSelectionEvent(grid, row, changedRows, evt);
                      }
                    });
                    grid.selection.selectAll = true;
                    service.decideRaiseSelectionBatchEvent(grid, changedRows, evt);
                  }
                },
                /**
                 * @ngdoc function
                 * @name clearSelectedRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Unselects all rows
                 * @param {Event} evt object if raised from an event
                 */
                clearSelectedRows: function (evt) {
                  service.clearSelectedRows(grid, evt);
                },
                /**
                 * @ngdoc function
                 * @name getSelectedRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description returns all selected Row's entity references
                 */
                getSelectedRows: function () {
                  return service.mapAndFilterRowsByEntity(service.getSelectedRows(grid));
                },
                /**
                 * @ngdoc function
                 * @name getUnSelectedRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description returns all unselected Row's entity references
                 */
                getUnSelectedRows: function () {
                  return service.mapAndFilterRowsByEntity(service.getUnSelectedRows(grid));
                },
                /**
                 * @ngdoc function
                 * @name getSelectedGridRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description returns all selectedRow's as gridRows
                 */
                getSelectedGridRows: function () {
                  return service.getSelectedRows(grid);
                },
                /**
                 * @ngdoc function
                 * @name getSelectedGridRows
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description returns all unselected Row's as gridRows
                 */
                getUnSelectedGridRows: function () {
                  return service.getUnSelectedRows(grid);
                },
                /**
                 * @ngdoc function
                 * @name getSelectedCount
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description returns the number of rows selected
                 */
                getSelectedCount: function () {
                  return grid.selection.selectedCount;
                },
                /**
                 * @ngdoc function
                 * @name setMultiSelect
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Sets the current gridOption.multiSelect to true or false
                 * @param {bool} multiSelect true to allow multiple rows
                 */
                setMultiSelect: function (multiSelect) {
                  grid.options.multiSelect = multiSelect;
                },
                /**
                 * @ngdoc function
                 * @name setModifierKeysToMultiSelect
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Sets the current gridOption.modifierKeysToMultiSelect to true or false
                 * @param {bool} modifierKeysToMultiSelect true to only allow multiple rows when using ctrlKey or shiftKey is used
                 */
                setModifierKeysToMultiSelect: function (modifierKeysToMultiSelect) {
                  grid.options.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
                },
                /**
                 * @ngdoc function
                 * @name getSelectAllState
                 * @methodOf  ui.grid.selection.api:PublicApi
                 * @description Returns whether or not the selectAll checkbox is currently ticked.  The
                 * grid doesn't automatically select rows when you add extra data - so when you add data
                 * you need to explicitly check whether the selectAll is set, and then call setVisible rows
                 * if it is
                 */
                getSelectAllState: function () {
                  return grid.selection.selectAll;
                }

              }
            }
          };

          grid.api.registerEventsFromObject(publicApi.events);

          grid.api.registerMethodsFromObject(publicApi.methods);

        },

        defaultGridOptions: function (gridOptions) {
          // default option to true unless it was explicitly set to false
          /**
           *  @ngdoc object
           *  @name ui.grid.selection.api:GridOptions
           *
           *  @description GridOptions for selection feature, these are available to be
           *  set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
           */

          /**
           *  @ngdoc object
           *  @name enableRowSelection
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable row selection for entire grid.
           *  <br/>Defaults to true
           */
          gridOptions.enableRowSelection = gridOptions.enableRowSelection !== false;
          /**
           *  @ngdoc object
           *  @name multiSelect
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable multiple row selection for entire grid
           *  <br/>Defaults to true
           */
          gridOptions.multiSelect = gridOptions.multiSelect !== false;
          /**
           *  @ngdoc object
           *  @name noUnselect
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Prevent a row from being unselected.  Works in conjunction
           *  with `multiselect = false` and `gridApi.selection.selectRow()` to allow
           *  you to create a single selection only grid - a row is always selected, you
           *  can only select different rows, you can't unselect the row.
           *  <br/>Defaults to false
           */
          gridOptions.noUnselect = gridOptions.noUnselect === true;
          /**
           *  @ngdoc object
           *  @name modifierKeysToMultiSelect
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable multiple row selection only when using the ctrlKey or shiftKey. Requires multiSelect to be true.
           *  <br/>Defaults to false
           */
          gridOptions.modifierKeysToMultiSelect = gridOptions.modifierKeysToMultiSelect === true;
          /**
           *  @ngdoc object
           *  @name enableRowHeaderSelection
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable a row header to be used for selection
           *  <br/>Defaults to true
           */
          gridOptions.enableRowHeaderSelection = gridOptions.enableRowHeaderSelection !== false;
          /**
           *  @ngdoc object
           *  @name enableFullRowSelection
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable selection by clicking anywhere on the row.  Defaults to
           *  false if `enableRowHeaderSelection` is true, otherwise defaults to true.
           */
          if (typeof (gridOptions.enableFullRowSelection) === 'undefined') {
            gridOptions.enableFullRowSelection = !gridOptions.enableRowHeaderSelection;
          }
          /**
           *  @ngdoc object
           *  @name enableFocusRowOnRowHeaderClick
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable focuse row by clicking on the row header.  Defaults to
           *  true if `enableRowHeaderSelection` is true, otherwise defaults to false.
           */
          gridOptions.enableFocusRowOnRowHeaderClick = (gridOptions.enableFocusRowOnRowHeaderClick !== false)
            || !gridOptions.enableRowHeaderSelection;
          /**
           *  @ngdoc object
           *  @name enableSelectRowOnFocus
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable focuse row by clicking on the row anywhere.  Defaults true.
           */
          gridOptions.enableSelectRowOnFocus = (gridOptions.enableSelectRowOnFocus !== false);
          /**
           *  @ngdoc object
           *  @name enableSelectAll
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Enable the select all checkbox at the top of the selectionRowHeader
           *  <br/>Defaults to true
           */
          gridOptions.enableSelectAll = gridOptions.enableSelectAll !== false;
          /**
           *  @ngdoc object
           *  @name enableSelectionBatchEvent
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description If selected rows are changed in bulk, either via the API or
           *  via the selectAll checkbox, then a separate event is fired.  Setting this
           *  option to false will cause the rowSelectionChanged event to be called multiple times
           *  instead
           *  <br/>Defaults to true
           */
          gridOptions.enableSelectionBatchEvent = gridOptions.enableSelectionBatchEvent !== false;
          /**
           *  @ngdoc object
           *  @name selectionRowHeaderWidth
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description can be used to set a custom width for the row header selection column
           *  <br/>Defaults to 30px
           */
          gridOptions.selectionRowHeaderWidth = angular.isDefined(gridOptions.selectionRowHeaderWidth) ? gridOptions.selectionRowHeaderWidth : 30;
          /**
           *  @ngdoc object
           *  @name enableFooterTotalSelected
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Shows the total number of selected items in footer if true.
           *  <br/>Defaults to true.
           *  <br/>GridOptions.showGridFooter must also be set to true.
           */
          gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false;

          /**
           *  @ngdoc object
           *  @name isRowSelectable
           *  @propertyOf  ui.grid.selection.api:GridOptions
           *  @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property.
           */
          gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop;
        },

        /**
         * @ngdoc function
         * @name toggleRowSelection
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Toggles row as selected or unselected
         * @param {Grid} grid grid object
         * @param {GridRow} row row to select or deselect
         * @param {Event} evt object if resulting from event
         * @param {bool} multiSelect if false, only one row at time can be selected
         * @param {bool} noUnselect if true then rows cannot be unselected
         * @param {bool} [canBeInvisible=true] if false, row can only be selected when it's (theoretically) visible
         */
         toggleRowSelection: function (grid, row, evt, multiSelect, noUnselect, canBeInvisible) {
          if ( row.enableSelection === false ) {
            return;
          }

          if (canBeInvisible === void 0) {
            canBeInvisible = true;
          }

          var selected = row.isSelected;

          if (!multiSelect) {
            if (!selected) {
              service.clearSelectedRows(grid, evt);
            }
            else if (service.getSelectedRows(grid).length > 1) {
              selected = false; // Enable reselect of the row
              service.clearSelectedRows(grid, evt);
            }
          }

          // only select row in this case
          if (!(selected && noUnselect) && (canBeInvisible || row.visible)) {
            row.setSelected(!selected);
            if (row.isSelected === true) {
              grid.selection.lastSelectedRow = row;
            }

            grid.selection.selectAll = grid.rows.length === service.getSelectedRows(grid).length;
            grid.api.selection.raise.rowSelectionChanged(row, evt);
          }
        },
        /**
         * @ngdoc function
         * @name shiftSelect
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description selects a group of rows from the last selected row using the shift key
         * @param {Grid} grid grid object
         * @param {GridRow} row clicked row
         * @param {Event} evt object if raised from an event
         * @param {bool} multiSelect if false, does nothing this is for multiSelect only
         */
        shiftSelect: function (grid, row, evt, multiSelect) {
          if (!multiSelect) {
            return;
          }
          var selectedRows = service.getSelectedRows(grid);
          var fromRow = selectedRows.length > 0 ? grid.renderContainers.body.visibleRowCache.indexOf(grid.selection.lastSelectedRow) : 0;
          var toRow = grid.renderContainers.body.visibleRowCache.indexOf(row);
          // reverse select direction
          if (fromRow > toRow) {
            var tmp = fromRow;
            fromRow = toRow;
            toRow = tmp;
          }

          var changedRows = [];
          for (var i = fromRow; i <= toRow; i++) {
            var rowToSelect = grid.renderContainers.body.visibleRowCache[i];
            if (rowToSelect) {
              if (!rowToSelect.isSelected && rowToSelect.enableSelection !== false) {
                rowToSelect.setSelected(true);
                grid.selection.lastSelectedRow = rowToSelect;
                service.decideRaiseSelectionEvent(grid, rowToSelect, changedRows, evt);
              }
            }
          }
          service.decideRaiseSelectionBatchEvent(grid, changedRows, evt);
        },
        /**
         * @ngdoc function
         * @name getSelectedRows
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Returns all the selected rows
         * @param {Grid} grid grid object
         */
        getSelectedRows: function (grid) {
          return grid.rows.filter(function (row) {
            return row.isSelected;
          });
        },
        /**
         * @ngdoc function
         * @name getUnSelectedRows
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Returns all the unselected rows
         * @param {Grid} grid grid object
         */
        getUnSelectedRows: function (grid) {
          return grid.rows.filter(function (row) {
            return !row.isSelected;
          });
        },
        /**
         * @ngdoc function
         * @name mapAndFilterRowsByEntity
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Filters all rows by entity and then maps them to Array.
         */
        mapAndFilterRowsByEntity: function(gridRows) {
          if (typeof gridRows.reduce === 'function') { // If reduce is available it will be taken, due to better performance
            return gridRows.reduce(function (previousVal, currentRow) {
              if (currentRow.entity.hasOwnProperty('$$hashKey') || !angular.isObject(currentRow.entity)) {
                previousVal.push(currentRow.entity);
              }
              return previousVal;
            }, []);
          }

          return gridRows.filter(function (gridRow) { // stays as polyfill
            return gridRow.entity.hasOwnProperty('$$hashKey') || !angular.isObject(gridRow.entity);
          }).map(function (gridRow) {
            return gridRow.entity;
          });
        },

        /**
         * @ngdoc function
         * @name clearSelectedRows
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Clears all selected rows
         * @param {Grid} grid grid object
         * @param {Event} evt object if raised from an event
         */
        clearSelectedRows: function (grid, evt) {
          var changedRows = [];
          service.getSelectedRows(grid).forEach(function (row) {
            if (row.isSelected && row.enableSelection !== false) {
              row.setSelected(false);
              service.decideRaiseSelectionEvent(grid, row, changedRows, evt);
            }
          });
          grid.selection.selectAll = false;
          grid.selection.selectedCount = 0;
          service.decideRaiseSelectionBatchEvent(grid, changedRows, evt);
        },

        /**
         * @ngdoc function
         * @name decideRaiseSelectionEvent
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Decides whether to raise a single event or a batch event
         * @param {Grid} grid grid object
         * @param {GridRow} row row that has changed
         * @param {array} changedRows an array to which we can append the changed
         * @param {Event} evt object if raised from an event
         * row if we're doing batch events
         */
        decideRaiseSelectionEvent: function (grid, row, changedRows, evt) {
          if (!grid.options.enableSelectionBatchEvent) {
            grid.api.selection.raise.rowSelectionChanged(row, evt);
          }
          else {
            changedRows.push(row);
          }
        },

        /**
         * @ngdoc function
         * @name raiseSelectionEvent
         * @methodOf  ui.grid.selection.service:uiGridSelectionService
         * @description Decides whether we need to raise a batch event, and
         * raises it if we do.
         * @param {Grid} grid grid object
         * @param {array} changedRows an array of changed rows, only populated
         * @param {Event} evt object if raised from an event
         * if we're doing batch events
         */
        decideRaiseSelectionBatchEvent: function (grid, changedRows, evt) {
          if (changedRows.length > 0) {
            grid.api.selection.raise.rowSelectionChangedBatch(changedRows, evt);
          }
        }
      };

      return service;
    });

  /**
   *  @ngdoc directive
   *  @name ui.grid.selection.directive:uiGridSelection
   *  @element div
   *  @restrict A
   *
   *  @description Adds selection features to grid
   *
   *  @example
   <example module="app">
   <file name="app.js">
   var app = angular.module('app', ['ui.grid', 'ui.grid.selection']);

   app.controller('MainCtrl', ['$scope', function ($scope) {
      $scope.data = [
        { name: 'Bob', title: 'CEO' },
            { name: 'Frank', title: 'Lowly Developer' }
      ];

      $scope.columnDefs = [
        {name: 'name', enableCellEdit: true},
        {name: 'title', enableCellEdit: true}
      ];
    }]);
   </file>
   <file name="index.html">
   <div ng-controller="MainCtrl">
   <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-selection></div>
   </div>
   </file>
   </example>
   */
  module.directive('uiGridSelection', ['i18nService', 'uiGridSelectionConstants', 'uiGridSelectionService', 'uiGridConstants',
    function (i18nService, uiGridSelectionConstants, uiGridSelectionService, uiGridConstants) {
      return {
        replace: true,
        priority: 0,
        require: '^uiGrid',
        scope: false,
        compile: function () {
          return {
            pre: function ($scope, $elm, $attrs, uiGridCtrl) {
              uiGridSelectionService.initializeGrid(uiGridCtrl.grid);
              if (uiGridCtrl.grid.options.enableRowHeaderSelection) {
                var selectionRowHeaderDef = {
                  name: uiGridSelectionConstants.selectionRowHeaderColName,
                  displayName: i18nService.getSafeText('selection.displayName'),
                  width: uiGridCtrl.grid.options.selectionRowHeaderWidth,
                  minWidth: 10,
                  cellTemplate: 'ui-grid/selectionRowHeader',
                  headerCellTemplate: 'ui-grid/selectionHeaderCell',
                  enableColumnResizing: false,
                  enableColumnMenu: false,
                  exporterSuppressExport: true,
                  allowCellFocus: true
                };

                uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef, 0);
              }

              var processorSet = false;

              var processSelectableRows = function (rows) {
                rows.forEach(function (row) {
                  row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row);
                });
                return rows;
              };

              var updateOptions = function () {
                if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) {
                  uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500);
                  processorSet = true;
                }
              };

              updateOptions();

              var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback(updateOptions, [uiGridConstants.dataChange.OPTIONS]);

              $scope.$on('$destroy', dataChangeDereg);
            },
            post: function ($scope, $elm, $attrs, uiGridCtrl) {

            }
          };
        }
      };
    }]);

  module.directive('uiGridSelectionRowHeaderButtons', ['$templateCache', 'uiGridSelectionService', 'gridUtil',
    function ($templateCache, uiGridSelectionService, gridUtil) {
      return {
        replace: true,
        restrict: 'E',
        template: $templateCache.get('ui-grid/selectionRowHeaderButtons'),
        scope: true,
        require: '^uiGrid',
        link: function ($scope, $elm, $attrs, uiGridCtrl) {
          var self = uiGridCtrl.grid;
          $scope.selectButtonClick = selectButtonClick;
          $scope.selectButtonKeyDown = selectButtonKeyDown;

          // On IE, prevent mousedowns on the select button from starting a selection.
          // If this is not done and you shift+click on another row, the browser will select a big chunk of text
          if (gridUtil.detectBrowser() === 'ie') {
            $elm.on('mousedown', selectButtonMouseDown);
          }

          function selectButtonKeyDown(row, evt) {
            if (evt.keyCode === 32 || evt.keyCode === 13) {
              evt.preventDefault();
              selectButtonClick(row, evt);
            }
          }

          function selectButtonClick(row, evt) {
            evt.stopPropagation();

            if (evt.shiftKey) {
              uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
            }
            else if (evt.ctrlKey || evt.metaKey) {
              uiGridSelectionService.toggleRowSelection(self, row, evt,
                self.options.multiSelect, self.options.noUnselect, false);
            }
            else if (row.groupHeader) {
              uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect, false);
              for (var i = 0; i < row.treeNode.children.length; i++) {
                uiGridSelectionService.toggleRowSelection(self, row.treeNode.children[i].row, evt, self.options.multiSelect, self.options.noUnselect, false);
              }
            }
            else {
              uiGridSelectionService.toggleRowSelection(self, row, evt,
                (self.options.multiSelect && !self.options.modifierKeysToMultiSelect), self.options.noUnselect, false);
            }
            self.options.enableFocusRowOnRowHeaderClick && row.setFocused(!row.isFocused) && self.api.selection.raise.rowFocusChanged(row, evt);
          }

          function selectButtonMouseDown(evt) {
            if (evt.ctrlKey || evt.shiftKey) {
              evt.target.onselectstart = function () { return false; };
              window.setTimeout(function () { evt.target.onselectstart = null; }, 0);
            }
          }

          $scope.$on('$destroy', function unbindEvents() {
            $elm.off();
          });
        }
      };
    }]);

  module.directive('uiGridSelectionSelectAllButtons', ['$templateCache', 'uiGridSelectionService',
    function ($templateCache, uiGridSelectionService) {
      return {
        replace: true,
        restrict: 'E',
        template: $templateCache.get('ui-grid/selectionSelectAllButtons'),
        scope: false,
        link: function ($scope) {
          var self = $scope.col.grid;

          $scope.headerButtonKeyDown = function (evt) {
            if (evt.keyCode === 32 || evt.keyCode === 13) {
              evt.preventDefault();
              $scope.headerButtonClick(evt);
            }
          };

          $scope.headerButtonClick = function (evt) {
            if (self.selection.selectAll) {
              uiGridSelectionService.clearSelectedRows(self, evt);
              if (self.options.noUnselect) {
                self.api.selection.selectRowByVisibleIndex(0, evt);
              }
              self.selection.selectAll = false;
            }
            else if (self.options.multiSelect) {
              self.api.selection.selectAllVisibleRows(evt);
              self.selection.selectAll = true;
            }
          };
        }
      };
    }]);

  /**
   *  @ngdoc directive
   *  @name ui.grid.selection.directive:uiGridViewport
   *  @element div
   *
   *  @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
   *  for the grid row
   */
  module.directive('uiGridViewport',
      function () {
        return {
          priority: -200, // run after default  directive
          scope: false,
          compile: function ($elm) {
            var rowRepeatDiv = angular.element($elm[0].querySelector('.ui-grid-canvas:not(.ui-grid-empty-base-layer-container)').children[0]),
              newNgClass = "'ui-grid-row-selected': row.isSelected, 'ui-grid-row-focused': row.isFocused}",
              existingNgClass = rowRepeatDiv.attr('ng-class');

            if (existingNgClass) {
              newNgClass = existingNgClass.slice(0, -1) + ',' + newNgClass;
            } else {
              newNgClass = '{' + newNgClass;
            }
            rowRepeatDiv.attr('ng-class', newNgClass);

            return {
              pre: function ($scope, $elm, $attrs, controllers) {},
              post: function ($scope, $elm, $attrs, controllers) {}
            };
          }
        };
      });

  /**
   *  @ngdoc directive
   *  @name ui.grid.selection.directive:uiGridCell
   *  @element div
   *  @restrict A
   *
   *  @description Stacks on top of ui.grid.uiGridCell to provide selection feature
   */
  module.directive('uiGridCell',
    ['uiGridConstants', 'uiGridSelectionService',
      function (uiGridConstants, uiGridSelectionService) {
        return {
          priority: -200, // run after default uiGridCell directive
          restrict: 'A',
          require: '?^uiGrid',
          scope: false,
          link: function ($scope, $elm, $attrs, uiGridCtrl) {
            var touchStartTime = 0,
              touchStartPos = {},
              touchTimeout = 300,
              touchPosDiff = 100;

            // Bind to keydown events in the render container
            if (uiGridCtrl.grid.api.cellNav) {
              uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
                if (rowCol === null ||
                  rowCol.row !== $scope.row ||
                  rowCol.col !== $scope.col) {
                  return;
                }

                if (evt.keyCode === uiGridConstants.keymap.SPACE && $scope.col.colDef.name === 'selectionRowHeaderCol') {
                  evt.preventDefault();
                  uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt,
                    ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect),
                    $scope.grid.options.noUnselect, false);
                  $scope.$apply();
                }
              });
            }

            var selectCells = function (evt) {
              // if you click on expandable icon doesn't trigger selection
              if (evt.target.className === "ui-grid-icon-minus-squared" || evt.target.className === "ui-grid-icon-plus-squared") {
                return;
              }

              // if we get a click, then stop listening for touchend
              $elm.off('touchend', touchEnd);

              if (evt.shiftKey) {
                uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect);
              }
              else if (evt.ctrlKey || evt.metaKey) {
                uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt,
                  $scope.grid.options.multiSelect, $scope.grid.options.noUnselect, false);
              }
              else if ($scope.grid.options.enableSelectRowOnFocus) {
                uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt,
                  ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect),
                  $scope.grid.options.noUnselect, false);
              }
              $scope.row.setFocused(!$scope.row.isFocused);
              $scope.grid.api.selection.raise.rowFocusChanged($scope.row, evt);
              $scope.$apply();

              // don't re-enable the touchend handler for a little while - some devices generate both, and it will
              // take a little while to move your hand from the mouse to the screen if you have both modes of input
              window.setTimeout(function () {
                $elm.on('touchend', touchEnd);
              }, touchTimeout);
            };

            var touchStart = function (evt) {
              touchStartTime = (new Date()).getTime();
              touchStartPos = evt.changedTouches[0];

              // if we get a touch event, then stop listening for click
              $elm.off('click', selectCells);
            };

            var touchEnd = function (evt) {
              var touchEndTime = (new Date()).getTime();
              var touchEndPos = evt.changedTouches[0];
              var touchTime = touchEndTime - touchStartTime;
              var touchXDiff = Math.abs(touchStartPos.clientX - touchEndPos.clientX)
              var touchYDiff = Math.abs(touchStartPos.clientY - touchEndPos.clientY)


              if (touchXDiff < touchPosDiff && touchYDiff < touchPosDiff) {
                if (touchTime < touchTimeout) {
                // short touch
                  selectCells(evt);
                }
              }

              // don't re-enable the click handler for a little while - some devices generate both, and it will
              // take a little while to move your hand from the screen to the mouse if you have both modes of input
              window.setTimeout(function () {
                $elm.on('click', selectCells);
              }, touchTimeout);
            };

            function registerRowSelectionEvents() {
              if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection && $scope.col.colDef.name !== 'selectionRowHeaderCol') {
                $elm.addClass('ui-grid-disable-selection');
                $elm.on('touchstart', touchStart);
                $elm.on('touchend', touchEnd);
                $elm.on('click', selectCells);

                $scope.registered = true;
              }
            }

            function unregisterRowSelectionEvents() {
              if ($scope.registered) {
                $elm.removeClass('ui-grid-disable-selection');
                $elm.off('touchstart', touchStart);
                $elm.off('touchend', touchEnd);
                $elm.off('click', selectCells);

                $scope.registered = false;
              }
            }

            registerRowSelectionEvents();

            // register a dataChange callback so that we can change the selection configuration dynamically
            // if the user changes the options
            var dataChangeUnreg = $scope.grid.registerDataChangeCallback(function () {
              if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection &&
                !$scope.registered) {
                registerRowSelectionEvents();
              }
              else if ((!$scope.grid.options.enableRowSelection || !$scope.grid.options.enableFullRowSelection) &&
                $scope.registered) {
                unregisterRowSelectionEvents();
              }
            }, [uiGridConstants.dataChange.OPTIONS]);

            $elm.on('$destroy', dataChangeUnreg);
          }
        };
      }]);

  module.directive('uiGridGridFooter', ['$compile', 'gridUtil', function ($compile, gridUtil) {
    return {
      restrict: 'EA',
      replace: true,
      priority: -1000,
      require: '^uiGrid',
      scope: true,
      compile: function () {
        return {
          pre: function ($scope, $elm, $attrs, uiGridCtrl) {
            if (!uiGridCtrl.grid.options.showGridFooter) {
              return;
            }

            gridUtil.getTemplate('ui-grid/gridFooterSelectedItems')
              .then(function (contents) {
                var template = angular.element(contents);

                var newElm = $compile(template)($scope);

                angular.element($elm[0].getElementsByClassName('ui-grid-grid-footer')[0]).append(newElm);
              });
          },
          post: function ($scope, $elm, $attrs, controllers) {

          }
        };
      }
    };
  }]);
})();
