goog.module('grrUi.core.pagedFilteredTableDirective');
goog.module.declareLegacyNamespace();

const itemsProviderController = goog.requireType('grrUi.core.itemsProviderController');



/**
 * Directive for displaying the top pane of the paged/filtered table.
 *
 * @return {!angular.Directive}
 * @ngInject
 * @export
 */
exports.TableTopDirective = function() {
  return {
    scope: {parentController: '='},
    require: '^grrPagedFilteredTable',
    templateUrl: '/static/angular-components/core/' +
        'paged-filtered-table-top.html',
    restrict: 'E'
  };
};


/**
 * Name of the directive as registered in Angular.
 *
 * @const
 * @export
 */
exports.TableTopDirective.directive_name = 'grrPagedFilteredTableTop';



/**
 * Directive for displaying the bottom part of the paged/filtered table.
 *
 * @return {!angular.Directive}
 * @ngInject
 * @export
 */
exports.TableBottomDirective = function() {
  return {
    scope: {parentController: '='},
    require: '^grrPagedFilteredTable',
    templateUrl: '/static/angular-components/core/' +
        'paged-filtered-table-bottom.html',
    restrict: 'E'
  };
};


/**
 * Name of the directive as registered in Angular.
 *
 * @const
 * @export
 */
exports.TableBottomDirective.directive_name = 'grrPagedFilteredTableBottom';



/**
 * Controller for PagedFilteredTableDirective.
 * @unrestricted
 */
const PagedFilteredTableController = class {
  /**
   * @param {!angular.Scope} $scope
   * @param {!angular.jQuery} $element
   * @param {function(function(angular.jQuery, angular.Scope), angular.jQuery)}
   *     $transclude
   * @param {!angular.$compile} $compile
   * @param {!angular.$interval} $interval
   * @ngInject
   */
  constructor($scope, $element, $transclude, $compile, $interval) {
    // Injected dependencies.

    /** @private {!angular.Scope} */
    this.scope_ = $scope;

    /** @type {?number} */
    this.scope_.pageSize;

    /** @type {?number} */
    this.scope_.autoRefreshInterval;

    /** @private {!angular.jQuery} */
    this.element_ = $element;

    /**
     * @private {function(function(angular.jQuery, angular.Scope),
     *     angular.jQuery)}
     */
    this.transclude_ = $transclude;

    /** @private {!angular.$compile} */
    this.compile_ = $compile;

    /** @private {!angular.$interval} */
    this.interval_ = $interval;

    // Internal state.

    /**
     * Items provider to be used for fetching items to be displayed.
     * @export {itemsProviderController.ItemsProviderController}
     */
    this.itemsProvider;

    /**
     * Currently shown page.
     * @export {number}
     */
    this.currentPage = 0;

    /**
     * Page currently selected by the pagination UI control. This is a pure UI
     * binding and has no connection with currentPage. Pagination control is
     * reset by assigning 1 to this variable.
     * @export {number}
     */
    this.paginationSelectedPage = 1;

    /**
     * If true, "Loading..." message should be shown.
     * @export {boolean}
     */
    this.showLoading = true;

    /**
     * Items currently shown.
     * @export {!Array<Object>}
     */
    this.items = [];

    /**
     * Total count of all items - not only the ones that are shown, but the
     * ones on the server.
     * @export {?number}
     */
    this.totalCount;

    /**
     * Number of elements shown on a single page.
     * @export {number}
     */
    this.pageSize =
        this.scope_.pageSize || PagedFilteredTableController.DEFAULT_PAGE_SIZE;

    /**
     * Used for UI binding with a filter edit field.
     * @export {string}
     */
    this.filterEditedValue = '';

    /**
     * Currently used filter value.
     * @export {string}
     */
    this.filterValue = '';

    /**
     * If true, filter is currently applied.
     * @export {boolean}
     */
    this.filterApplied = false;

    /**
     * If true, all the filtered items were fetched, there are no more left.
     * @export {boolean}
     */
    this.filterFinished = false;

    /**
     * Request counter used to ignore obsolete requests (request becomes
     * obsolete if another request gets sent as a result of a user action).
     * @private {number}
     */
    this.requestCounter_ = 0;

    /**
     * True, if there's an auto-refresh request currently in progress.
     * @private {boolean}
     */
    this.autoRefreshInProgress_ = false;

    this.addTopDirective_();
    this.addBottomDirective_();

    this.scope_.$watch('::controller.itemsProvider', function() {
      this.fetchUnfilteredItems(true);
    }.bind(this));

    if (this.scope_.autoRefreshInterval) {
      // Initialize the timer used to refresh data in the table.
      /** @type {!angular.$q.Promise} */
      var refreshTimer = this.interval_(
          this.onAutoRefresh_.bind(this), this.scope_.autoRefreshInterval);

      // Destroy the timer when the shared directive's scope is destroyed.
      this.scope_.$on('$destroy', function() {
        this.interval_.cancel(refreshTimer);
      }.bind(this));
    }
  }

  /**
   * Adds grr-paged-filtered-table-top directive before the table.
   *
   * @private
   */
  addTopDirective_() {
    var template = /** @type {angular.jQuery} */ (angular.element(
        '<grr-paged-filtered-table-top parent-controller="controller" />'));
    var table = this.element_.closest('table');
    template.insertBefore(table);
    this.compile_(template)(this.scope_);
  }

  /**
   * Adds grr-paged-filtered-table-bottom directive after the table.
   *
   * @private
   */
  addBottomDirective_() {
    var template = /** @type {angular.jQuery} */ (angular.element(
        '<grr-paged-filtered-table-bottom parent-controller="controller" />'));
    var table = this.element_.closest('table');
    template.insertAfter(table);
    this.compile_(template)(this.scope_);
  }

  /**
   * Wraps a given function with a check that ensures that it won't get executed
   * if the request counter changes between this call and the time when the
   * callback gets triggered.
   *
   * @param {Function} callback Promise callback to receive data.
   * @return {!Function} Wrapped callback.
   * @private
   */
  wrapWithCounterCheck_(callback) {
    this.requestCounter_ += 1;
    var curRequestCounter = this.requestCounter_;

    return function(data) {
      if (this.requestCounter_ == curRequestCounter) {
        return callback(data);
      }
    }.bind(this);
  }

  /**
   * Fetches unfiltered items for the current page.
   *
   * @param {boolean} withTotalCount If true, fetch total number of items on the
   *     server as well.
   * @export
   */
  fetchUnfilteredItems(withTotalCount) {
    this.showLoading = true;
    this.setItems_([]);

    this.itemsProvider
        .fetchItems(
            this.currentPage * this.pageSize, this.pageSize, withTotalCount)
        .then(this.wrapWithCounterCheck_(
            this.onFetchedUnfilteredItems_.bind(this)));
  }

  /**
   * Handles fetched unfiltered items.
   *
   * @param {!itemsProviderController.Items} items
   * @private
   */
  onFetchedUnfilteredItems_(items) {
    this.showLoading = false;
    this.setItems_(items.items);

    if (angular.isDefined(items.totalCount)) {
      this.totalCount = items.totalCount;
    }
  }

  /**
   * Applies currently typed in filter. If nothing is typed, fetches unfiltered
   * items, otherwise fetches filtered ones.
   *
   * @export
   */
  applyFilter() {
    this.filterValue = this.filterEditedValue;
    this.filterApplied = (this.filterValue != '');
    this.filterFinished = false;
    this.currentPage = 0;
    this.paginationSelectedPage = 1;
    this.setItems_([]);

    if (this.filterApplied) {
      this.fetchFilteredItems();
    } else {
      this.fetchUnfilteredItems(false);
    }
  }

  /**
   * Fetches specified number of pages of filtered items.
   *
   * @param {number=} opt_numPages How many pages of items to fetch.
   * @export
   */
  fetchFilteredItems(opt_numPages) {
    if (angular.isUndefined(opt_numPages)) {
      opt_numPages = 1;
    }
    this.showLoading = true;

    this.itemsProvider
        .fetchFilteredItems(
            this.filterValue, this.items.length, this.pageSize * opt_numPages)
        .then(this.wrapWithCounterCheck_(
            this.onFetchedFilteredItems_.bind(this)));
  }

  /**
   * Handles fetched filtered items.
   *
   * @param {!itemsProviderController.Items} items
   * @private
   */
  onFetchedFilteredItems_(items) {
    this.showLoading = false;
    this.setItems_(this.items.concat(items.items));

    if (items.items.length == 0 || items.items.length % this.pageSize != 0) {
      this.filterFinished = true;
    }
  }

  /**
   * Changes the items list, updating the presentation accordingly.
   *
   * @param {!Array<Object>} newItems
   * @private
   */
  setItems_(newItems) {
    var indexOffset = 0;
    if (angular.equals(this.items, newItems.slice(0, this.items.length))) {
      indexOffset = this.items.length;
    } else {
      this.element_.siblings().remove();
    }

    this.items = newItems;

    for (var i = indexOffset; i < this.items.length; ++i) {
      this.transclude_(function(clone, scope) {
        scope.item = this.items[i];
        scope.$index = i;
        this.element_.parent().append(clone);
      }.bind(this), this.element_.parent());
    }
  }

  /**
   * Triggered periodically if autoRefreshInterval binding is set.
   *
   * @private
   */
  onAutoRefresh_() {
    if (this.filterApplied || this.items.length >= this.pageSize ||
        this.autoRefreshInProgress_) {
      return;
    }

    var callback =
        this.wrapWithCounterCheck_(this.onFetchedUnfilteredItems_.bind(this));

    this.autoRefreshInProgress_ = true;
    this.itemsProvider
        .fetchItems(this.currentPage * this.pageSize, this.pageSize, true)
        .then(callback)
        .finally(function() {
          this.autoRefreshInProgress_ = false;
        }.bind(this));
  }

  /**
   * Handles changes in the UI page number binding.
   *
   * @param {number} newPage
   * @export
   */
  onPageChange(newPage) {
    this.currentPage = newPage;

    if (!this.filterApplied) {
      this.fetchUnfilteredItems(false);
    }
  }
};



/** @const */
PagedFilteredTableController.DEFAULT_PAGE_SIZE = 50;



/**
 * Directive for displaying paged tables with filtering capability.
 *
 * @return {!angular.Directive} Directive definition object.
 * @ngInject
 * @export
 */
exports.PagedFilteredTableDirective = function() {
  return {
    scope: {pageSize: '=?', autoRefreshInterval: '=?'},
    transclude: 'element',
    restrict: 'A',
    priority: 1000,
    terminal: true,
    require: ['?grrMemoryItemsProvider', '?grrApiItemsProvider'],
    controller: PagedFilteredTableController,
    controllerAs: 'controller',
    link: function(scope, element, attrs, controllers) {
      var providerController = undefined;
      for (var i = 0; i < controllers.length; ++i) {
        if (angular.isObject(controllers[i])) {
          if (angular.isDefined(providerController)) {
            throw new Error('Can have only 1 provider.');
          }
          providerController = controllers[i];
        }
      }
      if (angular.isUndefined(providerController)) {
        throw new Error('Data provider not specified.');
      }

      scope.controller.itemsProvider = providerController;
    }
  };
};


/**
 * Name of the directive as registered in Angular.
 *
 * @const
 * @export
 */
exports.PagedFilteredTableDirective.directive_name = 'grrPagedFilteredTable';
