goog.module('grrUi.semantic.semanticProtoDirective');
goog.module.declareLegacyNamespace();

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



/**
 * Returns value of a union type field in a union structure.
 *
 * @param {!Object} value Value to be converted to an array of items.
 * @param {!Object} descriptor Descriptor of the value.
 * @return {string} Union field value.
 * @throws {Error} when value and descriptor do not correspond to a union-type
 *     structure or when value of the union type field can't be determined
 *     (the latter shouldn't happen and means either a logic bug or a broken
 *     data structure).
 * @export
 */
exports.getUnionFieldValue = function(value, descriptor) {
  const unionFieldName = descriptor['union_field'];
  if (angular.isUndefined(unionFieldName)) {
    throw new Error('Not a union-type structure.');
  }

  if (angular.isDefined(value['value'][unionFieldName])) {
    return value['value'][unionFieldName]['value'].toLowerCase();
  } else {
    const fieldsLength = descriptor['fields'].length;
    for (let i = 0; i < fieldsLength; ++i) {
      const field = descriptor['fields'][i];
      if (field['name'] == unionFieldName) {
        return field['default']['value'].toLowerCase();
      }
    }
  }

  throw new Error('Can\'t determine value of the union field.');
};
const getUnionFieldValue = exports.getUnionFieldValue;


/**
 * Builds a list of items to display for union-type structures. These
 * structures have a field that determines which of its nested structures
 * should be used.
 *
 * @param {!Object} value Value to be converted to an array of items.
 * @param {!Object} descriptor Descriptor of the value to be converted to an
 *     array of items. Expected to have 'fields' attribute with list of fields
 *     descriptors.
 * @return {!Array.<Object>} List of items to display. It will *always* have
 *     *only* union type field and a field that union type field value points
 *     to.
 * @export
 */
exports.buildUnionItems = function(value, descriptor) {
  const items = [];

  const unionFieldName = descriptor['union_field'];
  const unionFieldValue = getUnionFieldValue(value, descriptor);

  const fieldsLength = descriptor['fields'].length;
  for (let i = 0; i < fieldsLength; ++i) {
    const field = descriptor['fields'][i];
    const key = field['name'];
    if (key !== unionFieldName && key !== unionFieldValue) {
      continue;
    }
    let keyValue = value.value[key];
    if (angular.isUndefined(keyValue)) {
      keyValue = field['default'];
    }

    items.push({
      'value': keyValue,
      'key': field['friendly_name'] || field['name'],
      'desc': field['doc']
    });
  }

  return items;
};
const buildUnionItems = exports.buildUnionItems;


/**
 * Builds a list of items to display from a given non-union-type value. If value
 * has type descriptors, friendly names will be used as keys and
 * description will be filled in.
 *
 * @param {!Object} value Value to be converted to an array of items.
 * @param {!Object} descriptor Descriptor of the value to be converted to an
 *     array of items. Expected to have 'fields' attribute with list of fields
 *     descriptors.
 * @param {Object=} opt_visibleFields If provided, only shows fields with names
 *     from this list.
 * @param {Object=} opt_hiddenFields If provided, doesn't show fields with names
 *     from this list.
 * @return {!Array.<Object>} List of items to display.
 * @export
 */
exports.buildNonUnionItems = function(
    value, descriptor, opt_visibleFields, opt_hiddenFields) {
  if (angular.isUndefined(descriptor['fields'])) {
    return [];
  }

  const items = [];

  const fieldsLength = descriptor['fields'].length;
  for (let i = 0; i < fieldsLength; ++i) {
    const field = descriptor['fields'][i];
    const key = field['name'];
    let keyValue = value.value[key];

    if (opt_visibleFields && opt_visibleFields.indexOf(key) == -1) {
      continue;
    }

    if (opt_hiddenFields && opt_hiddenFields.indexOf(key) != -1) {
      continue;
    }

    if (angular.isUndefined(keyValue)) {
      if (!opt_visibleFields) {
        continue;
      } else {
        keyValue = angular.copy(field['default']);
      }
    }
    items.push({
      'value': keyValue,
      'key': field['friendly_name'] || field['name'],
      'structKey': field['name'],
      'desc': field['doc'],
      'fieldDescriptor': field,
    });
  }

  return items;
};
const buildNonUnionItems = exports.buildNonUnionItems;


/**
 * Builds a list of items to display from the given value. If value
 * has type descriptors, friendly names will be used as keys and
 * description will be filled in.
 *
 * @param {!Object} value Value to be converted to an array of items.
 * @param {!Object} descriptor Descriptor of the value to be converted to an
 *     array of items. Expected to have 'fields' attribute with list of fields
 *     descriptors.
 * @param {Object=} opt_visibleFields If provided, only shows fields with names
 *     from this list.
 * @param {Object=} opt_hiddenFields If provided, doesn't show fields with names
 *     from this list.
 * @return {!Array.<Object>} List of items to display.
 * @export
 */
exports.buildItems = function(
    value, descriptor, opt_visibleFields, opt_hiddenFields) {
  if (angular.isDefined(descriptor['union_field'])) {
    return buildUnionItems(value, descriptor);
  } else {
    return buildNonUnionItems(
        value, descriptor, opt_visibleFields, opt_hiddenFields);
  }
};
const buildItems = exports.buildItems;

/**
 * Controller for SemanticProtoDirective.
 * @unrestricted
 */
const SemanticProtoController = class {
  /**
   * @param {!angular.Scope} $scope Directive's scope.
   * @param {!reflectionService.ReflectionService}
   *     grrReflectionService
   * @ngInject
   */
  constructor($scope, grrReflectionService) {
    /** @private {!angular.Scope} */
    this.scope_ = $scope;

    /** @private {!reflectionService.ReflectionService} */
    this.grrReflectionService_ = grrReflectionService;

    /** @export {Array.<Object>} */
    this.items;

    this.scope_.$watch('::value', this.onValueChange.bind(this));
  }

  /**
   * Handles value changes.
   *
   * @param {Object} newValue
   * @param {Object} oldValue
   * @export
   */
  onValueChange(newValue, oldValue) {
    // newValue and oldValue are both undefined if the watcher is called to do
    // initialization before the value binding is actually set. In this case
    // we have to do nothing and wait until the watcher is called with a real
    // value.
    if (newValue === undefined && oldValue === undefined) {
      return;
    }

    if (angular.isObject(this.scope_['value'])) {
      const valueType = this.scope_['value']['type'];
      this.grrReflectionService_.getRDFValueDescriptor(valueType).then(
          function success(descriptor) {
            this.items = buildItems(
                this.scope_['value'], descriptor, this.scope_['visibleFields'],
                this.scope_['hiddenFields']);
          }.bind(this));  // TODO(user): Reflection failure scenario should
                          // be handled globally by reflection service.
    } else {
      this.items = [];
    }
  }
};



/**
 * Directive that displays semantic proto fetched from the server.
 *
 * @return {!angular.Directive} Directive definition object.
 * @ngInject
 * @export
 */
exports.SemanticProtoDirective = function() {
  return {
    scope: {value: '=', visibleFields: '=', hiddenFields: '='},
    restrict: 'E',
    templateUrl: '/static/angular-components/semantic/semantic-proto.html',
    controller: SemanticProtoController,
    controllerAs: 'controller'
  };
};


/**
 * Name of the directive in Angular.
 *
 * @const
 * @export
 */
exports.SemanticProtoDirective.directive_name = 'grrSemanticProto';

/**
 * Semantic type corresponding to this directive.
 *
 * @const
 * @export
 */
exports.SemanticProtoDirective.semantic_type = 'RDFProtoStruct';
