goog.module('grrUi.flow.flowsListDirective');
goog.module.declareLegacyNamespace();

const {InfiniteTableController} = goog.require('grrUi.core.infiniteTableDirective');



var TABLE_KEY_NAME = InfiniteTableController.UNIQUE_KEY_NAME;
var TABLE_ROW_HASH = InfiniteTableController.ROW_HASH_NAME;


/** @type {number} */
let AUTO_REFRESH_INTERVAL_MS = 30 * 1000;

/**
 * Sets the delay between automatic refreshes of the flow list.
 *
 * @param {number} millis Interval value in milliseconds.
 * @export
 */
exports.setAutoRefreshInterval = function(millis) {
  AUTO_REFRESH_INTERVAL_MS = millis;
};


/** @const {number} */
const PAGE_SIZE = 1000;

/**
 * Flattens list of flows returned by the server. Every flow in the list
 * can have 'nested_flows' attribute with a list of child flows. This
 * function flattens this tree-like structure into a simple list and
 * assigns depth to every element in the list.
 *
 * @param {!Array<Object>} flows List of flows to flatten.
 * @param {number=} opt_currentDepth Current depth, defaults to 0.
 * @return {!Array<Object>} Flattened list of flows.
 *
 * @export
 */
exports.flattenFlowsList = function(flows, opt_currentDepth) {
  if (angular.isUndefined(opt_currentDepth)) {
    opt_currentDepth = 0;
  }

  var result = [];
  for (var i = 0; i < flows.length; ++i) {
    var flow = angular.copy(flows[i]);
    flow['depth'] = opt_currentDepth;

    result.push(flow);
    if (angular.isDefined(flow['value']) &&
        angular.isDefined(flow['value']['nested_flows'])) {
      result = result.concat(exports.flattenFlowsList(
          flow['value']['nested_flows'], opt_currentDepth + 1));

      delete flow['value']['nested_flows'];
    }
  }
  return result;
};
var flattenFlowsList = exports.flattenFlowsList;


/**
 * Toggles 'shown' attribute on flows[index]. When making the flow visible,
 * all direct children become visible. Indirect children become visible only
 * if their respective parents were previously expanded. When hiding the flow,
 * all direct and indirect children become hidden.
 *
 * @param {Array<Object>} flows Flattened list of flows.
 * @param {number} index Index of a flow to be expanded.
 * @return {!Array<Object>} Returns list of flows passed in "flows" argument.
 *     Note that it's modified inplace, no copies are created.
 *
 * @export
 */
exports.toggleFlowExpansion = function(flows, index) {
  var flowToExpand = flows[index];
  var i;

  flowToExpand.expanded = !flowToExpand.expanded;

  if (!flowToExpand.expanded) {
    for (i = index + 1; i < flows.length; ++i) {
      if (flows[i].depth > flowToExpand.depth) {
        flows[i].shown = false;
      } else {
        break;
      }
    }
  } else {
    // If this is not null, ignore all flows with a depth > ignoreDepth.
    var ignoreDepth = null;
    for (i = index + 1; i < flows.length; ++i) {
      if (flows[i].depth > flowToExpand.depth) {
        if (!ignoreDepth || ignoreDepth >= flows[i].depth) {
          flows[i].shown = true;

          // If this flow was expanded, we want to show its immediate children,
          // so we set ignoreDepth to null. Otherwise we set ignoreDepth
          // to flows's depth. This way all direct and indirect children of
          // flows[i] will get ignored and their "shown" attribute won't get
          // flipped.
          //
          // It's important to note that when ignoreDepth is set to
          // flow's depth, flow's sibling flows are not affected, as they have
          // depth which is equal to ignoreDepth (see the condition above).
          //
          // Example:
          // A (depth = 0, expanded = false)
          //   B (depth = 1, expanded = false)
          //     C (depth = 2, expanded = false)
          //   D (depth = 1, expanded = false)
          //
          // Let's say toggleFlowExpansion is called on A:
          // 1. B.shown is set to true. ignoreDepth is set to 1, because
          //    B.expanded is False.
          // 2. C is skipped, because its depth (2) is greater than
          //    ignoreDepth (1).
          // 3. D.shown is set to true, because its depth (1) is equal
          //    to ignoredepth (1). ignoreDepth is set to 1.
          ignoreDepth = flows[i].expanded ? null : flows[i].depth;
        }

      } else {
        break;
      }
    }
  }

  return flows;
};
var toggleFlowExpansion = exports.toggleFlowExpansion;


/**
 * Controller for FlowsListDirective.
 * @unrestricted
 */
const FlowsListController = class {
  /**
   * @param {!angular.Scope} $scope
   * @param {!angular.jQuery} $element
   * @param {!grrUi.core.apiService.ApiService} grrApiService
   * @ngInject
   */
  constructor($scope, $element, grrApiService) {
    /** @private {!angular.Scope} */
    this.scope_ = $scope;

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

    /** @type {!Object<string, Object>} */
    this.flowsById = {};

    /** @type {?string} */
    this.selectedFlowId;

    /** @type {function(boolean)} */
    this.triggerTableUpdate;

    /** @type {number} */
    this.autoRefreshInterval = AUTO_REFRESH_INTERVAL_MS;

    /** @type {number} */
    this.pageSize = PAGE_SIZE;

    // Push the selection changes back to the scope, so that other UI components
    // can react on the change.
    this.scope_.$watch('controller.selectedFlowId', function(newValue) {
      // Only propagate real changes, don't propagate initial undefined
      // value.
      if (angular.isDefined(newValue)) {
        this.scope_['selectedFlowId'] = newValue;
      }
    }.bind(this));

    // If outer binding changes, we want to update our selection.
    this.scope_.$watch('selectedFlowId', function(newValue) {
      if (angular.isDefined(newValue)) {
        this.selectedFlowId = newValue;
      }
    }.bind(this));

    // Propagate our triggerUpdate implementation to the scope so that users of
    // this directive can use it.
    this.scope_['triggerUpdate'] = this.triggerUpdate.bind(this);
  }

  /**
   * Selects given item in the list.
   *
   * @param {!Object} item Item to be selected.
   * @export
   */
  selectItem(item) {
    this.selectedFlowId = item['value']['flow_id']['value'];
  }

  /**
   * Transforms items fetched by API items provider so that they can be
   * correctly presented as flows tree.
   *
   * @param {!Array<Object>} items Items to be transformed.
   * @return {!Array<Object>} Transformed items.
   * @export
   */
  transformItems(items) {
    var flattenedItems = flattenFlowsList(items);

    angular.forEach(flattenedItems, function(item, index) {
      var components = item['value']['flow_id']['value'].split('/');
      item.shortId = components[components.length - 1];
      item.shown = item.depth == 0;
      if (index < flattenedItems.length - 1 &&
          flattenedItems[index + 1].depth > item.depth) {
        item.expanded = false;
      }

      item.expand = function(e) {
        e.stopPropagation();
        toggleFlowExpansion(flattenedItems, index);
      };

      // If the flow couldn't be parsed by the AdminUI, it won't have
      // proper "state" and "last_active_at" attributes. Getting
      // these attributes conditionally.
      // NOTE: It's only safe to assume that the "flow_id" attribute
      // is present.
      var state = 'BROKEN';
      if (angular.isDefined(item['value']['state'])) {
        state = item['value']['state']['value'];
      }

      var last_active_at = 0;
      if (angular.isDefined(item['value']['last_active_at'])) {
        last_active_at = item['value']['last_active_at']['value'];
      }

      item[TABLE_KEY_NAME] = item['value']['flow_id']['value'];
      item[TABLE_ROW_HASH] = [state, last_active_at];
    }.bind(this));

    return flattenedItems;
  }

  /**
   * Triggers a graceful update of the infinite table.
   *
   * @export
   */
  triggerUpdate() {
    this.triggerTableUpdate(true);
  }
};



/**
 * FlowsListDirective definition.

 * @return {angular.Directive} Directive definition object.
 */
exports.FlowsListDirective = function() {
  return {
    scope: {flowsUrl: '=', selectedFlowId: '=?', triggerUpdate: '=?'},
    restrict: 'E',
    templateUrl: '/static/angular-components/flow/flows-list.html',
    controller: FlowsListController,
    controllerAs: 'controller'
  };
};


/**
 * Directive's name in Angular.
 *
 * @const
 * @export
 */
exports.FlowsListDirective.directive_name = 'grrFlowsList';
