text: define(["require", "exports", "./validator", "./validate-trigger", "./property-info", "./validate-result", "./property-accessor-parser", "./validate-event"], function (require, exports, validator_1, validate_trigger_1, property_info_1, validate_result_1, property_accessor_parser_1, validate_event_1) {
      "use strict";
      Object.defineProperty(exports, "__esModule", { value: true });
      /**
       * Orchestrates validation.
       * Manages a set of bindings, renderers and objects.
       * Exposes the current list of validation results for binding purposes.
       */
      var ValidationController = /** @class */ (function () {
          function ValidationController(validator, propertyParser) {
              this.validator = validator;
              this.propertyParser = propertyParser;
              // Registered bindings (via the validate binding behavior)
              this.bindings = new Map();
              // Renderers that have been added to the controller instance.
              this.renderers = [];
              /**
               * Validation results that have been rendered by the controller.
               */
              this.results = [];
              /**
               * Validation errors that have been rendered by the controller.
               */
              this.errors = [];
              /**
               *  Whether the controller is currently validating.
               */
              this.validating = false;
              // Elements related to validation results that have been rendered.
              this.elements = new Map();
              // Objects that have been added to the controller instance (entity-style validation).
              this.objects = new Map();
              /**
               * The trigger that will invoke automatic validation of a property used in a binding.
               */
              this.validateTrigger = validate_trigger_1.validateTrigger.blur;
              // Promise that resolves when validation has completed.
              this.finishValidating = Promise.resolve();
              this.eventCallbacks = [];
          }
          /**
           * Subscribe to controller validate and reset events. These events occur when the
           * controller's "validate"" and "reset" methods are called.
           * @param callback The callback to be invoked when the controller validates or resets.
           */
          ValidationController.prototype.subscribe = function (callback) {
              var _this = this;
              this.eventCallbacks.push(callback);
              return {
                  dispose: function () {
                      var index = _this.eventCallbacks.indexOf(callback);
                      if (index === -1) {
                          return;
                      }
                      _this.eventCallbacks.splice(index, 1);
                  }
              };
          };
          /**
           * Adds an object to the set of objects that should be validated when validate is called.
           * @param object The object.
           * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules.
           */
          ValidationController.prototype.addObject = function (object, rules) {
              this.objects.set(object, rules);
          };
          /**
           * Removes an object from the set of objects that should be validated when validate is called.
           * @param object The object.
           */
          ValidationController.prototype.removeObject = function (object) {
              this.objects.delete(object);
              this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []);
          };
          /**
           * Adds and renders an error.
           */
          ValidationController.prototype.addError = function (message, object, propertyName) {
              if (propertyName === void 0) { propertyName = null; }
              var resolvedPropertyName;
              if (propertyName === null) {
                  resolvedPropertyName = propertyName;
              }
              else {
                  resolvedPropertyName = this.propertyParser.parse(propertyName);
              }
              var result = new validate_result_1.ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message);
              this.processResultDelta('validate', [], [result]);
              return result;
          };
          /**
           * Removes and unrenders an error.
           */
          ValidationController.prototype.removeError = function (result) {
              if (this.results.indexOf(result) !== -1) {
                  this.processResultDelta('reset', [result], []);
              }
          };
          /**
           * Adds a renderer.
           * @param renderer The renderer.
           */
          ValidationController.prototype.addRenderer = function (renderer) {
              var _this = this;
              this.renderers.push(renderer);
              renderer.render({
                  kind: 'validate',
                  render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }),
                  unrender: []
              });
          };
          /**
           * Removes a renderer.
           * @param renderer The renderer.
           */
          ValidationController.prototype.removeRenderer = function (renderer) {
              var _this = this;
              this.renderers.splice(this.renderers.indexOf(renderer), 1);
              renderer.render({
                  kind: 'reset',
                  render: [],
                  unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); })
              });
          };
          /**
           * Registers a binding with the controller.
           * @param binding The binding instance.
           * @param target The DOM element.
           * @param rules (optional) rules associated with the binding. Validator implementation specific.
           */
          ValidationController.prototype.registerBinding = function (binding, target, rules) {
              this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null });
          };
          /**
           * Unregisters a binding with the controller.
           * @param binding The binding instance.
           */
          ValidationController.prototype.unregisterBinding = function (binding) {
              this.resetBinding(binding);
              this.bindings.delete(binding);
          };
          /**
           * Interprets the instruction and returns a predicate that will identify
           * relevant results in the list of rendered validation results.
           */
          ValidationController.prototype.getInstructionPredicate = function (instruction) {
              var _this = this;
              if (instruction) {
                  var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules;
                  var predicate_1;
                  if (instruction.propertyName) {
                      predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; };
                  }
                  else {
                      predicate_1 = function (x) { return x.object === object_1; };
                  }
                  if (rules_1) {
                      return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); };
                  }
                  return predicate_1;
              }
              else {
                  return function () { return true; };
              }
          };
          /**
           * Validates and renders results.
           * @param instruction Optional. Instructions on what to validate. If undefined, all
           * objects and bindings will be validated.
           */
          ValidationController.prototype.validate = function (instruction) {
              var _this = this;
              // Get a function that will process the validation instruction.
              var execute;
              if (instruction) {
                  // tslint:disable-next-line:prefer-const
                  var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules;
                  // if rules were not specified, check the object map.
                  rules_2 = rules_2 || this.objects.get(object_2);
                  // property specified?
                  if (instruction.propertyName === undefined) {
                      // validate the specified object.
                      execute = function () { return _this.validator.validateObject(object_2, rules_2); };
                  }
                  else {
                      // validate the specified property.
                      execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); };
                  }
              }
              else {
                  // validate all objects and bindings.
                  execute = function () {
                      var promises = [];
                      for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) {
                          var _b = _a[_i], object = _b[0], rules = _b[1];
                          promises.push(_this.validator.validateObject(object, rules));
                      }
                      for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) {
                          var _e = _d[_c], binding = _e[0], rules = _e[1].rules;
                          var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source);
                          if (!propertyInfo || _this.objects.has(propertyInfo.object)) {
                              continue;
                          }
                          promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules));
                      }
                      return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); });
                  };
              }
              // Wait for any existing validation to finish, execute the instruction, render the results.
              this.validating = true;
              var returnPromise = this.finishValidating
                  .then(execute)
                  .then(function (newResults) {
                  var predicate = _this.getInstructionPredicate(instruction);
                  var oldResults = _this.results.filter(predicate);
                  _this.processResultDelta('validate', oldResults, newResults);
                  if (returnPromise === _this.finishValidating) {
                      _this.validating = false;
                  }
                  var result = {
                      instruction: instruction,
                      valid: newResults.find(function (x) { return !x.valid; }) === undefined,
                      results: newResults
                  };
                  _this.invokeCallbacks(instruction, result);
                  return result;
              })
                  .catch(function (exception) {
                  // recover, to enable subsequent calls to validate()
                  _this.validating = false;
                  _this.finishValidating = Promise.resolve();
                  return Promise.reject(exception);
              });
              this.finishValidating = returnPromise;
              return returnPromise;
          };
          /**
           * Resets any rendered validation results (unrenders).
           * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results
           * will be unrendered.
           */
          ValidationController.prototype.reset = function (instruction) {
              var predicate = this.getInstructionPredicate(instruction);
              var oldResults = this.results.filter(predicate);
              this.processResultDelta('reset', oldResults, []);
              this.invokeCallbacks(instruction, null);
          };
          /**
           * Gets the elements associated with an object and propertyName (if any).
           */
          ValidationController.prototype.getAssociatedElements = function (_a) {
              var object = _a.object, propertyName = _a.propertyName;
              var elements = [];
              for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) {
                  var _c = _b[_i], binding = _c[0], target = _c[1].target;
                  var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source);
                  if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) {
                      elements.push(target);
                  }
              }
              return elements;
          };
          ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) {
              // prepare the instruction.
              var instruction = {
                  kind: kind,
                  render: [],
                  unrender: []
              };
              // create a shallow copy of newResults so we can mutate it without causing side-effects.
              newResults = newResults.slice(0);
              var _loop_1 = function (oldResult) {
                  // get the elements associated with the old result.
                  var elements = this_1.elements.get(oldResult);
                  // remove the old result from the element map.
                  this_1.elements.delete(oldResult);
                  // create the unrender instruction.
                  instruction.unrender.push({ result: oldResult, elements: elements });
                  // determine if there's a corresponding new result for the old result we are unrendering.
                  var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; });
                  if (newResultIndex === -1) {
                      // no corresponding new result... simple remove.
                      this_1.results.splice(this_1.results.indexOf(oldResult), 1);
                      if (!oldResult.valid) {
                          this_1.errors.splice(this_1.errors.indexOf(oldResult), 1);
                      }
                  }
                  else {
                      // there is a corresponding new result...
                      var newResult = newResults.splice(newResultIndex, 1)[0];
                      // get the elements that are associated with the new result.
                      var elements_1 = this_1.getAssociatedElements(newResult);
                      this_1.elements.set(newResult, elements_1);
                      // create a render instruction for the new result.
                      instruction.render.push({ result: newResult, elements: elements_1 });
                      // do an in-place replacement of the old result with the new result.
                      // this ensures any repeats bound to this.results will not thrash.
                      this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult);
                      if (!oldResult.valid && newResult.valid) {
                          this_1.errors.splice(this_1.errors.indexOf(oldResult), 1);
                      }
                      else if (!oldResult.valid && !newResult.valid) {
                          this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult);
                      }
                      else if (!newResult.valid) {
                          this_1.errors.push(newResult);
                      }
                  }
              };
              var this_1 = this;
              // create unrender instructions from the old results.
              for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) {
                  var oldResult = oldResults_1[_i];
                  _loop_1(oldResult);
              }
              // create render instructions from the remaining new results.
              for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) {
                  var result = newResults_1[_a];
                  var elements = this.getAssociatedElements(result);
                  instruction.render.push({ result: result, elements: elements });
                  this.elements.set(result, elements);
                  this.results.push(result);
                  if (!result.valid) {
                      this.errors.push(result);
                  }
              }
              // render.
              for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) {
                  var renderer = _c[_b];
                  renderer.render(instruction);
              }
          };
          /**
           * Validates the property associated with a binding.
           */
          ValidationController.prototype.validateBinding = function (binding) {
              if (!binding.isBound) {
                  return;
              }
              var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source);
              var rules;
              var registeredBinding = this.bindings.get(binding);
              if (registeredBinding) {
                  rules = registeredBinding.rules;
                  registeredBinding.propertyInfo = propertyInfo;
              }
              if (!propertyInfo) {
                  return;
              }
              var object = propertyInfo.object, propertyName = propertyInfo.propertyName;
              this.validate({ object: object, propertyName: propertyName, rules: rules });
          };
          /**
           * Resets the results for a property associated with a binding.
           */
          ValidationController.prototype.resetBinding = function (binding) {
              var registeredBinding = this.bindings.get(binding);
              var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source);
              if (!propertyInfo && registeredBinding) {
                  propertyInfo = registeredBinding.propertyInfo;
              }
              if (registeredBinding) {
                  registeredBinding.propertyInfo = null;
              }
              if (!propertyInfo) {
                  return;
              }
              var object = propertyInfo.object, propertyName = propertyInfo.propertyName;
              this.reset({ object: object, propertyName: propertyName });
          };
          /**
           * Changes the controller's validateTrigger.
           * @param newTrigger The new validateTrigger
           */
          ValidationController.prototype.changeTrigger = function (newTrigger) {
              this.validateTrigger = newTrigger;
              var bindings = Array.from(this.bindings.keys());
              for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) {
                  var binding = bindings_1[_i];
                  var source = binding.source;
                  binding.unbind();
                  binding.bind(source);
              }
          };
          /**
           * Revalidates the controller's current set of errors.
           */
          ValidationController.prototype.revalidateErrors = function () {
              for (var _i = 0, _a = this.errors; _i < _a.length; _i++) {
                  var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule;
                  if (rule.__manuallyAdded__) {
                      continue;
                  }
                  var rules = [[rule]];
                  this.validate({ object: object, propertyName: propertyName, rules: rules });
              }
          };
          ValidationController.prototype.invokeCallbacks = function (instruction, result) {
              if (this.eventCallbacks.length === 0) {
                  return;
              }
              var event = new validate_event_1.ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result);
              for (var i = 0; i < this.eventCallbacks.length; i++) {
                  this.eventCallbacks[i](event);
              }
          };
          ValidationController.inject = [validator_1.Validator, property_accessor_parser_1.PropertyAccessorParser];
          return ValidationController;
      }());
      exports.ValidationController = ValidationController;
  });
  