define(['angular', 'liveTableFilters', 'pagination'], function(angular) {
  var liveTable = angular.module('liveTable', ['liveTableFilters', 'pagination']);

  liveTable.directive('livetable',  ['$location', '$timeout', function($location, $timeout) {
    var key = function(id, name) {
      return id + name.substr(0, 1).toUpperCase() + name.substr(1);
    };

    var getFilterParamName = function(filterName) {
      return '.' + filterName;
    };

    var getSelectedColumn = function(columns) {
      var firstSortableColumn = null;
      for (var i = 0; i < columns.length; i++) {
        if (columns[i].selected) {
          return columns[i];
        } else if (!firstSortableColumn && columns[i].sortable) {
          firstSortableColumn = columns[i];
        }
      }
      // If no column is explicitly selected then select the first sortable column.
      return firstSortableColumn;
    };

    // 'this' should be the directive scope.
    var updateParamsFromURL = function() {
      // Read the parameter values from the URL.
      var urlParams = $location.search();

      this.params = {
        offset: parseInt(urlParams[key(this.id, 'offset')]) || 0,
        limit: 15,
        sort: urlParams[key(this.id, 'sort')],
        order: urlParams[key(this.id, 'order')]
      };

      if (!this.params.sort) {
        var selectedColumn = getSelectedColumn(this.columns);
        if (selectedColumn) {
          this.params.sort = selectedColumn.id;
          this.params.order = selectedColumn.order;
        }
      }

      this.filter = {};
      angular.forEach(this.columns, function(column) {
        if (column.filterable) {
          var paramName = getFilterParamName(column.id);
          this.filter[column.id] = urlParams[key(this.id, paramName)];
          this.params[paramName] = this.filter[column.id];
        }
      }, this);
    };

    var storeParams = function(id, params) {
      // Save the parameters in the URL in order to have a bookmarkable state.
      for (var param in params) {
        $location.search(key(id, param), params[param]);
      }
    };

    return {
      restrict: 'E',
      scope: {
        'id': '@',
        'source': '=',
        'columns': '=',
        'selection': '=?'
      },
      templateUrl: new XWiki.Document('LiveTable', 'FileManagerCode').getURL('get'),
      link: function(scope, element, attrs) {
        // This can be made configurable.
        scope.liveTableFilterTemplate = new XWiki.Document('LiveTableFilter', 'FileManagerCode').getURL('get');
        scope.liveTableCellTemplate = new XWiki.Document('LiveTableCell', 'FileManagerCode').getURL('get');

        // Update the live table whenever the source changes.
        scope.$watch('source', function(newValue, oldValue) {
          // Reset the selected rows.
          if (scope.selection) {
            scope.selection = {};
            // Unfortunately the ngTrueValue directive supports only constant expressions so we can't bind the row
            // object to the checkbox value. All the data from the selected rows must be available so we use an
            // workaround: we keep an internal map with the selected row ids and we update the selection whenever it
            // changes.
            scope.selectedRowIds = {};
          }
          if (newValue === oldValue) {
            // Initialize the live table parameters and filters only after the source is initialized.
            // The live table id is not defined otherwise and thus we can't read the URL parameters.
            // The parameter changes will trigger a live table update.
            updateParamsFromURL.call(scope);
          } else if (scope.params.offset !== 0) {
            // Reset the pagination. This will trigger a live table update.
            scope.params.offset = 0;
          } else {
            // Force a live table update preserving the current parameters.
            scope.rows = scope.source.get(scope.params);
          }
        });

        // Update the live table whenever the parameters are modified.
        scope.$watch('params', function(newValue, oldValue) {
          storeParams(scope.id, newValue);
          scope.rows = scope.source ? scope.source.get(newValue) : [];
        }, true);

        // Update the parameters whenever the URL changes.
        scope.$on('$locationChangeSuccess', angular.bind(scope, updateParamsFromURL));

        scope.sort = function(column) {
          if (column.sortable) {
            scope.params.order = (column.id == scope.params.sort && scope.params.order != 'desc') ? 'desc' : null;
            scope.params.sort = column.id;
          }
        };

        // The live table filters should not trigger an update immediately, but after a delay. We achieve this by
        // deferring the update when the filters are changed and by cancelling the pending update whenever a new change
        // is detected.
        var deferredUpdate = null;
        scope.$watch('filter', function(newValue, oldValue) {
          $timeout.cancel(deferredUpdate);
          deferredUpdate = $timeout(function() {
            // We trigger a live table update by adding the filters to the live table parameters.
            for(var filterName in scope.filter) {
              var filterValue = scope.filter[filterName];
              if (filterValue === '') {
                filterValue = undefined;
              }
              // Make sure the filter name doesn't collide with the default live table parameter names.
              scope.params[getFilterParamName(filterName)] = filterValue;
            }
          }, 500);
        }, true);

        scope.updateSelectedValue = function(row) {
          if (scope.selectedRowIds[row.id] === true) {
            // All the data from the selected rows must be available.
            scope.selection[row.id] = row;
          } else {
            delete scope.selection[row.id];
          }
        };
      }
    };
  }]);

  liveTable.directive('wrapIf',  function() {
    return {
      restrict: 'A',
      scope: {
        wrapIf: '='
      },
      link: function(scope, element, attrs) {
        if (!scope.wrapIf) {
          // Replace the element with its contents.
          var domElement = element[0];
          while (domElement.firstChild) {
            domElement.parentNode.insertBefore(domElement.firstChild, domElement);
          }
          element.remove();
        }
      }
    }
  });
});
