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

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


/**
 * @type {Object<string,
 *     function(!angular.Scope, function(Object, !angular.Scope=)=):Object>}
 * Cache for templates used by semantic value directive.
 */
let singleValueTemplateCache = {};


/**
 * @type {(function(!angular.Scope, function(Object,
 *     !angular.Scope=)=):Object|undefined)}
 * Precached template for lists of values.
 */
let repeatedValuesTemplate;


/**
 * Clears cached templates.
 *
 * @export
 */
exports.clearCaches = function() {
  singleValueTemplateCache = {};
  repeatedValuesTemplate = undefined;
};

/**
 * Gets value of the cached single value template.
 *
 * @param {string} name Name of the template to lookup in the cache.
 * @return {function(!angular.Scope, function(Object, !angular.Scope=)=):Object}
 *
 * @export
 */
exports.getCachedSingleValueTemplate = function(name) {
  return singleValueTemplateCache[name];
};


/**
 * Controller for the RegistryOverrideDirective.
 * @unrestricted
 */
const RegistryOverrideController = class {
  /**
   * @param {!angular.Scope} $scope
   * @ngInject
   */
  constructor($scope) {
    /** @private {!angular.Scope} */
    this.scope_ = $scope;

    /** @type {Object<string, Object>} */
    this.map;

    /** @type {string} */
    this.overrideKey;

    this.scope_.$watch(function() {
      return this.map;
    }.bind(this), this.onMapChange_.bind(this));
  }

  /**
   * Handles changes of the 'map' binding and calculates unique map identifier
   * to be used in the templates cache.
   *
   * @param {Object} newValue
   * @private
   */
  onMapChange_(newValue) {
    this.overrideKey = '';
    angular.forEach(
        Object.keys(/** @type {!Object} */ (this.map)).sort(), function(key) {
          const value = this.map[key];
          this.overrideKey += ':' + key + '_' + value['directive_name'];
        }.bind(this));
  }
};



/**
 * RegistryOverrideDirective allows users to override "RDF type <-> directive"
 * bindings registered in grrSemanticValueDirectivesRegistryService.
 *
 * @return {!angular.Directive} Directive definition object.
 */
exports.RegistryOverrideDirective = function() {
  return {
    scope: {map: '='},
    controller: RegistryOverrideController,
    bindToController: true,
    restrict: 'E',
    transclude: true,
    template: '<ng-transclude></ng-transclude>'
  };
};


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


/**
 * Controller for the SemanticValueDirective.
 * @unrestricted
 */
const SemanticValueController = class {
  /**
   * @param {!angular.Scope} $scope
   * @param {!angular.$compile} $compile
   * @param {!jQuery} $element
   * @param {!semanticRegistryService.SemanticRegistryService}
   *     grrSemanticValueDirectivesRegistryService
   * @ngInject
   */
  constructor(
      $scope, $compile, $element, grrSemanticValueDirectivesRegistryService) {
    /** @private {!angular.Scope} */
    this.scope_ = $scope;

    /** @type {?} */
    this.scope_.value;

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

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

    /**
     * @private {!semanticRegistryService.SemanticRegistryService}
     */
    this.grrSemanticValueDirectivesRegistryService_ =
        grrSemanticValueDirectivesRegistryService;

    /** @type {RegistryOverrideController} */
    this.registryOverrideController;

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

  /**
   * Converts camelCaseStrings to dash-delimited-strings.
   *
   * @param {string} directiveName String to be converted.
   * @return {string} Converted string.
   */
  camelCaseToDashDelimited(directiveName) {
    return directiveName.replace(/\W+/g, '-')
        .replace(/([a-z\d])([A-Z])/g, '$1-$2')
        .toLowerCase();
  }

  /**
   * Compiles a template for a given single value.
   *
   * @param {Object} value Value to compile the template for.
   * @return {!angular.$q.Promise} Promise that will get resolved into the
   *     compiled template.
   * @private
   */
  compileSingleTypedValueTemplate_(value) {
    const successHandler = function(directive) {
      const element = angular.element('<span />');

      element.html(
          '<' + this.camelCaseToDashDelimited(directive.directive_name) +
          ' value="::value" />');
      return this.compile_(element);
    }.bind(this);

    const failureHandler = function(directive) {
      const element = angular.element('<span />');
      element.html('{$ ::value.value $}');
      return this.compile_(element);
    }.bind(this);

    let overrides;
    if (this.registryOverrideController) {
      overrides = this.registryOverrideController.map;
    }

    return this.grrSemanticValueDirectivesRegistryService_
        .findDirectiveForType(value['type'], overrides)
        .then(successHandler, failureHandler);
  }

  /**
   * Compiles a template for repeated values.
   *
   * @return {function(!angular.Scope, function(Object,
   *     !angular.Scope=)=):Object} Compiled template.
   * @private
   */
  compileRepeatedValueTemplate_() {
    const element = angular.element(
        '<div ng-repeat="item in ::repeatedValue || []">' +
        '<grr-semantic-value value="::item" /></div>');
    return this.compile_(element);
  }

  /**
   * Handles value changes.
   *
   * @export
   */
  onValueChange() {
    const value = this.scope_.value;

    if (value == null) {
      return;
    }

    /**
     * @type {(function(!angular.Scope, function(Object,
     *     !angular.Scope=)=):Object|undefined)}
     */
    let template;


    if (angular.isDefined(value['type'])) {
      const handleTemplate = function(template) {
        template(this.scope_, function(cloned, opt_scope) {
          this.element_.html('');
          this.element_.append(cloned);
        }.bind(this));
      }.bind(this);

      // Make sure that templates for overrides do not collide with either
      // templates for other overrides or with default templates.
      let cacheKey = value['type'];
      if (this.registryOverrideController) {
        cacheKey += this.registryOverrideController.overrideKey;
      }

      template = singleValueTemplateCache[cacheKey];
      if (angular.isUndefined(template)) {
        this.compileSingleTypedValueTemplate_(value).then(function(tmpl) {
          singleValueTemplateCache[cacheKey] = tmpl;
          handleTemplate(tmpl);
        }.bind(this));
      } else {
        handleTemplate(template);
      }
    } else if (angular.isArray(value)) {
      if (value.length > 10) {
        const continuation = value.slice(10);
        this.scope_.repeatedValue = value.slice(0, 10);
        this.scope_.repeatedValue.push(
            {type: '__FetchMoreLink', value: continuation});
      } else {
        this.scope_.repeatedValue = value;
      }

      if (angular.isUndefined(repeatedValuesTemplate)) {
        repeatedValuesTemplate = this.compileRepeatedValueTemplate_();
      }
      template = repeatedValuesTemplate;

      template(this.scope_, function(cloned, opt_scope) {
        this.element_.html('');
        this.element_.append(cloned);
      }.bind(this));
    } else {
      this.element_.text(value.toString() + ' ');
    }
  }
};



/**
 * SemanticValueDirective renders given RDFValue by applying type-specific
 * renderers to its fields. It's assumed that RDFValue is fetched with
 * type info information.
 *
 * @return {!angular.Directive} Directive definition object.
 */
exports.SemanticValueDirective = function() {
  return {
    scope: {value: '='},
    require: '?^grrSemanticValueRegistryOverride',
    restrict: 'E',
    controller: SemanticValueController,
    controllerAs: 'controller',
    link: function(
        scope, element, attrs, grrSemanticValueRegistryOverrideCtrl) {
      if (grrSemanticValueRegistryOverrideCtrl) {
        scope['controller']['registryOverrideController'] =
            grrSemanticValueRegistryOverrideCtrl;
      }
    }
  };
};


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