goog.module('grrUi.docs.apiDocsDirective');
goog.module.declareLegacyNamespace();

const {ApiService, stripTypeInfo} = goog.require('grrUi.core.apiService');


/**
 * Controller for ApiDocsDirective.
 * @unrestricted
 */
const ApiDocsController = class {
  /**
   * @param {!angular.jQuery} $element
   * @param {angular.$http} $http The Angular http service.
   * @param {!ApiService} grrApiService
   * @ngInject
   */
  constructor($element, $http, grrApiService) {
    /** @private {!angular.jQuery} */
    this.element_ = $element;

    /** @private {angular.$http} */
    this.http_ = $http;

    /** @private {!ApiService} */
    this.grrApiService_ = grrApiService;

    /** @export {!Object<string, Array<Object>>} */
    this.apiMethodsByCategory;

    /** @export {Object<string, *>} */
    this.examplesByMethod;

    /** @export {Array<string>} */
    this.categories;

    /** @export {string} */
    this.visibleCategory;

    this.grrApiService_.get('reflection/api-methods')
        .then(this.onApiMethodsListFetched_.bind(this));
    this.http_.get('/static/angular-components/docs/api-docs-examples.json')
        .then(this.onExamplesFetched_.bind(this));
  }

  /**
   * Handles response to the docs API request.
   *
   * @param {!Object} response
   * @private
   */
  onApiMethodsListFetched_(response) {
    var categoriesDict = {};
    var items = stripTypeInfo(response['data']['items']);
    angular.forEach(/** @type {Array<Object>} */ (items), function(descriptor) {
      var category = descriptor['category'];
      if (!category) {
        category = 'Other';
      }

      if (categoriesDict[category] === undefined) {
        categoriesDict[category] = [];
      }
      categoriesDict[category].push(descriptor);
    }.bind(this));

    angular.forEach(categoriesDict, function(descriptors) {
      descriptors.sort(function(a, b) {
        var astr = a['name'] + '_' + a['http_route'];
        var bstr = b['name'] + '_' + b['http_route'];
        if (astr > bstr) {
          return 1;
        } else if (astr < bstr) {
          return -1;
        } else {
          return 0;
        }
      });
    }.bind(this));
    this.apiMethodsByCategory = categoriesDict;
    this.categories = Object.keys(categoriesDict).sort();
    this.visibleCategory = this.categories[0];
  }

  /**
   * Handles response to the api-docs-examples.json request.
   *
   * @param {!Object} response
   * @private
   */
  onExamplesFetched_(response) {
    this.examplesByMethod = {};
    angular.forEach(response.data, function(handlerExamples) {
      angular.forEach(handlerExamples, function(example) {
        var apiMethod = example['api_method'];
        if (!this.examplesByMethod[apiMethod]) {
          this.examplesByMethod[apiMethod] = [];
        }
        this.examplesByMethod[apiMethod].push(example);
      }.bind(this));
    }.bind(this));
  }

  /**
   * Handles clicks on category links.
   *
   * @param {string} category Category name.
   * @export
   */
  onCategoryLinkClick(category) {
    var index = this.categories.indexOf(category);
    var headingElement = $('#docs-category-' + index.toString());

    /**
     * We have to find a scrollable container that actually has the scrollbars.
     * This container may be anywhere up in the ancestors hierarchy.
     * Scrollable container has to satisfy 2 conditions:
     * 1) It should either be already scrolled (scrollTop() > 0) or it's
     *    scrollable height should be more than its physical height.
     * 2) scrollTop() values should change when scrollTop(offset) is called.
     *    Changing scrollTop() value means that container has actually
     *    scrolled.
     */
    var scrollableContainer = headingElement.parent();
    while (scrollableContainer.length != 0) {
      if (scrollableContainer.scrollTop() > 0 ||
          Math.abs(
              scrollableContainer[0].clientHeight -
              scrollableContainer[0].scrollHeight) > 1) {
        var offset = headingElement.offset()['top'] -
            scrollableContainer.offset()['top'] +
            scrollableContainer.scrollTop();
        var prevOffset = scrollableContainer.scrollTop();
        scrollableContainer.scrollTop(/** @type {number} */ (offset));

        /**
         * This approach is a bit hacky, but seems to work. Container is truly
         * scrollable if it's scrollTop() value changes after scrollTop(offset)
         * was called. Therefore we stop searching for scrollable parent
         * if this condition is satisfied.
         *
         * One exception to the approach above is when container is already
         * scrolled to the right place. Then we also consider the container
         * to be the scrollable container we were searching for and break.
         * Probability of having 2 containers scrolled to exactly the same
         * point is low, so even though this algorithm is not strictly
         * deterministic, it's fine for the needs of the docs page.
         */
        if (scrollableContainer.scrollTop() != prevOffset ||
            Math.abs(prevOffset - offset) <= 1) {
          break;
        }
      }
      scrollableContainer = scrollableContainer.parent();
    }
  }
};



/**
 * Directive for displaying API documentation.
 *
 * @return {!angular.Directive} Directive definition object.
 * @ngInject
 * @export
 */
exports.ApiDocsDirective = function() {
  return {
    scope: {},
    restrict: 'E',
    templateUrl: '/static/angular-components/docs/api-docs.html',
    controller: ApiDocsController,
    controllerAs: 'controller'
  };
};


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