import { AbstractControl } from '@angular/forms';

import {
  _convertToPromise, _executeValidators, _executeAsyncValidators, _mergeObjects,
  _mergeErrors, isEmpty, isDefined, hasValue, isString, isNumber, isBoolean,
  isArray, getType, isType, toJavaScriptType, xor,
  SchemaPrimitiveType, PlainObject, IValidatorFn, AsyncIValidatorFn
} from './validator.functions';
import { forEachCopy } from './utility.functions';

/**
 * 'JsonValidators' module
 *
 * Provides an extended set of validators to be used by form controls,
 * compatible with standard JSON Schema validation options.
 * http://json-schema.org/latest/json-schema-validation.html
 *
 * Note: This library is designed as a drop-in replacement for the Angular 2
 * Validators library, and except for one small breaking change to the 'pattern'
 * validator (described below) it can even be imported as a substitute, like so:
 *
 *   import { JsonValidators as Validators } from 'json-validators';
 *
 * and it should work with existing code as a complete replacement.
 *
 * The one exception is the 'pattern' validator, which has been changed to
 * matche partial values by default (the standard 'pattern' validator wrapped
 * all patterns in '^' and '$', forcing them to always match an entire value).
 * However, the old behavior can be restored by simply adding '^' and '$'
 * around your patterns, or by passing an optional second parameter of TRUE.
 * This change is to make the 'pattern' validator match the behavior of a
 * JSON Schema pattern, which allows partial matches, rather than the behavior
 * of an HTML input control pattern, which does not.
 *
 * This library replaces Angular 2's 4 validators and 1 validator combination
 * function with the following 16 validators and 4 transformation functions:
 *
 * Validators:
 * For all formControls:     required (*), type, enum
 * For text formControls:    minLength (*), maxLength (*), pattern (*), format
 * For numeric formControls: minimum, maximum, multipleOf
 * For formGroup objects:    minProperties, maxProperties, dependencies
 * For formArray arrays:     minItems, maxItems, uniqueItems
 * (Validators originally included with Angular 2 are maked with (*).)
 *
 * NOTE: The dependencies validator is not complete.
 * NOTE: The enum validator does not yet work with objects.
 *
 * Validator transformation functions:
 *   composeAnyOf, composeOneOf, composeAllOf, composeNot
 * (Angular 2's original combination funciton, 'compose', is also included for
 * backward compatibility, though it is effectively equivalent to composeAllOf,
 * though with a more generic error message.)
 *
 * All validators have also been extended to accept an optional second argument
 * which, if passed a TRUE value, causes the validator to perform the opposite
 * of its original finction. (This is used internally to enable 'not' and
 * 'composeOneOf' to function and return useful error messages.)
 *
 * The 'required' validator has also been overloaded so that if called with
 * a boolean parameter (or no parameters) it returns the original validator
 * function (rather than executing it). However, if it is called with an
 * AbstractControl parameter (as was previously required), it behaves
 * exactly as before.
 *
 * This enables all validators (including 'required') to be constructed in
 * exactly the same way, so they can be automatically applied using the
 * equivalent key names and values taken directly from a JSON Schema.
 *
 * This source code is partially derived from Angular 2,
 * which is Copyright (c) 2014-2016 Google, Inc.
 * Use of this source code is therefore governed by the same MIT-style license
 * that can be found in the LICENSE file at https://angular.io/license
 */

export class JsonValidators {

  /**
   * Validator functions:
   *
   * For all formControls:     required, type, enum
   * For text formControls:    minLength, maxLength, pattern, format
   * For numeric formControls: minimum, maximum, multipleOf
   * For formGroup objects:    minProperties, maxProperties, dependencies
   * For formArray arrays:     minItems, maxItems, uniqueItems
   *
   * TODO: finish dependencies validator
   * TODO: update enum to work with formGroup objects
   */

  /**
   * 'required' validator
   *
   * This validator is overloaded, compared to the default required validator.
   * If called with no parameters, or TRUE, this validator returns the
   * 'required' validator function (rather than executing it). This matches
   * the behavior of all other validators in this library.
   *
   * If this validator is called with an AbstractControl parameter
   * (as was previously required) it behaves the same as Angular 2's default
   * required validator, and returns an error if the control is empty.
   *
   * Old behavior: (if input type = AbstractControl)
   * @param {AbstractControl} control - required control
   * @return {{[key: string]: boolean}} - returns error message if no input
   *
   * New behavior: (if no input, or input type = boolean)
   * @param {boolean = true} required? - true to validate, false to disable
   * @return {IValidatorFn} - returns the 'required' validator function itself
   */
  static required(input: AbstractControl): PlainObject;
  static required(input?: boolean): IValidatorFn;

  static required(input?: AbstractControl | boolean): PlainObject | IValidatorFn {
    if (input === undefined) { input = true; }
    switch (input) {
      case true: // Return required function (do not execute it yet)
        return (control: AbstractControl, invert: boolean = false): PlainObject => {
          if (invert) { return null; } // if not required, always return valid
          return hasValue(control.value) ? null : { 'required': true };
        };
      case false: // Do nothing
        return (control: AbstractControl): PlainObject => null;
      default: // Execute required function
        return hasValue((<AbstractControl>input).value) ?
          null : { 'required': true };
    }
  };

  /**
   * 'type' validator
   *
   * Requires a control to only accept values of a specified type,
   * or one of an array of types.
   *
   * Note: SchemaPrimitiveType = 'string'|'number'|'integer'|'boolean'|'null'
   *
   * @param {SchemaPrimitiveType | SchemaPrimitiveType[]} type - type(s) to accept
   * @return {IValidatorFn}
   */
  static type(type: SchemaPrimitiveType | SchemaPrimitiveType[]): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualValue: any = control.value;
      let typeArray: SchemaPrimitiveType[] = isArray(type) ?
        <SchemaPrimitiveType[]>type : [<SchemaPrimitiveType>type];
      let isValid: boolean = false;
      for (let typeValue of typeArray) {
        if (isType(actualValue, typeValue) === true) {
          isValid = true; break;
        }
      }
      return xor(isValid, invert) ?
        null : { 'type': { type, actualValue } };
    };
  }

  /**
   * 'enum' validator
   *
   * Requires a control to have a value from an enumerated list of values.
   *
   * Converts types as needed to allow string inputs to still correctly
   * match number, boolean, and null enum values.
   * (toJavaScriptType() can be used later to convert these string values.)
   *
   * TODO: modify to work with objects
   *
   * @param {any[]} enumList - array of acceptable values
   * @return {IValidatorFn}
   */
  static enum(enumList: any[]): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let isValid: boolean = true;
      let actualValues: any | any[] = (isArray(control.value)) ?
        control.value : [control.value];
      for (let i1 = 0, l1 = actualValues.length; i1 < l1; i1++) {
        let actualValue: any = actualValues[i1];
        let itemIsValid: boolean = false;
        for (let i2 = 0, l2 = enumList.length; i2 < l2; i2++) {
          let enumValue: any = enumList[i2];
          if (actualValue === enumValue) {
            itemIsValid = true; break;
          } else if (isNumber(enumValue) && +actualValue === +enumValue) {
            itemIsValid = true; break;
          } else if (
            isBoolean(enumValue, 'strict') &&
            toJavaScriptType(actualValue, 'boolean') === enumValue
          ) {
            itemIsValid = true; break;
          } else if (enumValue === null && !hasValue(actualValue)) {
            itemIsValid = true; break;
          }
        }
        if (!itemIsValid) { isValid = false; break; }
      }
      return xor(isValid, invert) ?
        null : { 'enum': { 'enum': enumList, 'actualValue': control.value } };
    };
  }

  /**
   * 'minLength' validator
   *
   * Requires a control's text value to be greater than a specified length.
   *
   * @param {number} requiredLength - minimum allowed string length
   * @param {boolean = false} invert - instead return error object only if valid
   * @return {IValidatorFn}
   */
  static minLength(requiredLength: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualLength: number = isString(control.value) ? control.value.length : 0;
      let isValid: boolean = actualLength >= requiredLength;
      return xor(isValid, invert) ?
        null :
        { 'minlength': { requiredLength, actualLength } };
    };
  };

  /**
   * 'maxLength' validator
   *
   * Requires a control's text value to be less than a specified length.
   *
   * @param {number} requiredLength - maximum allowed string length
   * @param {boolean = false} invert - instead return error object only if valid
   * @return {IValidatorFn}
   */
  static maxLength(requiredLength: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let actualLength: number = isString(control.value) ? control.value.length : 0;
      let isValid: boolean = actualLength <= requiredLength;
      return xor(isValid, invert) ?
        null :
        { 'maxlength': { requiredLength, actualLength } };
    };
  };

  /**
   * 'pattern' validator
   *
   * Note: NOT the same as Angular 2's default pattern validator.
   * Requires a control's value to match a specified regular expression pattern.
   *
   * This validator changes the behavior of default pattern validator
   * by replacing RegExp(`^${pattern}$`) with RegExp(`${pattern}`),
   * which allows for partial matches.
   *
   * To return to the default funcitonality, and match the entire string,
   * pass TRUE as the optional second parameter.
   *
   * @param {string} pattern - regular expression pattern
   * @param {boolean = false} wholeString - match whole value string?
   * @return {IValidatorFn}
   */
  static pattern(pattern: string, wholeString: boolean = false): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualValue: string = control.value;
      let requiredPattern: string = (wholeString) ? `^${pattern}$` : pattern;
      let regex = new RegExp(requiredPattern);
      let isValid: boolean = isString(actualValue) ? regex.test(actualValue) : false;
      return xor(isValid, invert) ?
        null : { 'pattern': { requiredPattern, actualValue } };
    };
  }

  /**
   * 'format' validator
   *
   * Requires a control to have a value of a certain format.
   *
   * This validator currently checks the following formsts:
   * 'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'
   *
   * TODO: add 'regex' and 'color' formats
   *
   * @param {'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'} format - format to check
   * @return {IValidatorFn}
   */
  static format(
    format: 'date-time' | 'email' | 'hostname' | 'ipv4' | 'ipv6' | 'uri' | 'url' | 'color'
  ): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let isValid: boolean;
      let actualValue: string = control.value;
      if (!isString(actualValue)) {
        isValid = false;
      } else {
        switch (format) {
          case 'date-time':
            isValid = !!actualValue.match(/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/);
            break;
          case 'email':
            let parts: string[] = actualValue.split('@');
            isValid =
              !!parts && parts.length === 2 &&
              !!parts[0].match(/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")$/)
              &&
              !!parts[1].match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
            break;
          case 'hostname':
            isValid = !!actualValue.match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
            break;
          case 'ipv4':
            isValid = !!actualValue.match(/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/);
            break;
          case 'ipv6':
            isValid = !!actualValue.match(/(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))/);
            break;
          case 'uri': case 'url':
            isValid = !!actualValue.match(/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)$/);
            break;
          case 'color':
            isValid = !!actualValue.match(/^#[A-Fa-f0-9]{6}$/);
            break;
          default:
            console.error('format validator error: "' + format +
              '" is not a recognized format.');
            isValid = true;
        }
      }
      return xor(isValid, invert) ?
        null : { 'format': { format, actualValue } };
    };
  }

  /**
   * 'minimum' validator
   *
   * Requires a control to have a numeric value not greater than
   * a specified minimum amount.
   *
   * The optional second parameter indicates whether the valid range excludes
   * the minimum value. It defaults to false, and includes the minimum.
   *
   * @param {number} minimum - minimum allowed value
   * @param {boolean = false} exclusiveMinimum - include minimum value itself?
   * @return {IValidatorFn}
   */
  static minimum(minimum: number, exclusiveMinimum: boolean = false): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualValue: number = control.value;
      let isValid: boolean = isNumber(actualValue) &&
        exclusiveMinimum ? actualValue > minimum : actualValue >= minimum;
      return xor(isValid, invert) ?
        null : { 'minimum': { minimum, exclusiveMinimum, actualValue } };
    };
  }

  /**
   * 'maximum' validator
   *
   * Requires a control to have a numeric value not less than
   * a specified maximum amount.
   *
   * The optional second parameter indicates whether the valid range excludes
   * the maximum value. It defaults to false, and includes the maximum.
   *
   * @param {number} maximum - maximum allowed value
   * @param {boolean = false} exclusiveMaximum - include maximum value itself?
   * @return {IValidatorFn}
   */
  static maximum(maximum: number, exclusiveMaximum: boolean = false): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualValue: number = control.value;
      let isValid: boolean = isNumber(actualValue) &&
        exclusiveMaximum ? actualValue < maximum : actualValue <= maximum;
      return xor(isValid, invert) ?
        null : { 'maximum': { maximum, exclusiveMaximum, actualValue } };
    };
  }

  /**
   * 'multipleOf' validator
   *
   * Requires a control to have a numeric value that is a multiple
   * of a specified number.
   *
   * @param {number} multipleOf - number value must be a multiple of
   * @return {IValidatorFn}
   */
  static multipleOf(multipleOf: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualValue: number = control.value;
      let isValid: boolean = isNumber(actualValue) && actualValue % multipleOf === 0;
      return xor(isValid, invert) ?
        null : { 'multipleOf': { multipleOf, actualValue } };
    };
  }

  /**
   * 'minProperties' validator
   *
   * Requires a form group to have a minimum number of properties (i.e. have
   * values entered in a minimum number of controls within the group).
   *
   * @param {number} minProperties - minimum number of properties allowed
   * @return {IValidatorFn}
   */
  static minProperties(minProperties: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualProperties: number = Object.keys(control.value).length || 0;
      let isValid: boolean = actualProperties >= minProperties;
      return xor(isValid, invert) ?
        null : { 'minProperties': { minProperties, actualProperties } };
    };
  }

  /**
   * 'maxProperties' validator
   *
   * Requires a form group to have a maximum number of properties (i.e. have
   * values entered in a maximum number of controls within the group).
   *
   * Note: Has no effect if the form group does not contain more than the
   * maximum number of controls.
   *
   * @param {number} maxProperties - maximum number of properties allowed
   * @return {IValidatorFn}
   */
  static maxProperties(maxProperties: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let actualProperties: number = Object.keys(control.value).length || 0;
      let isValid: boolean = actualProperties <= maxProperties;
      return xor(isValid, invert) ?
        null : { 'maxProperties': { maxProperties, actualProperties } };
    };
  }

  /**
   * 'dependencies' validator
   *
   * Requires the controls in a form group to meet additional validation
   * criteria, depending on the values of other controls in the group.
   *
   * Examples:
   * https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies
   *
   * @param {any} dependencies - required dependencies
   * @return {IValidatorFn}
   */
  static dependencies(dependencies: any): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      if (getType(dependencies) !== 'object' || isEmpty(dependencies)) { return null; }
      let allErrors: PlainObject = _mergeObjects(
        forEachCopy(dependencies, (value, requiringField) => {
          if (!hasValue(control.value[requiringField])) { return null; }
          let requiringFieldErrors: PlainObject = { };
          let requiredFields: string[];
          let properties: PlainObject = { };
          if (getType(dependencies[requiringField]) === 'array') {
            requiredFields = dependencies[requiringField];
          } else if (getType(dependencies[requiringField]) === 'object') {
            requiredFields = dependencies[requiringField]['required'] || [];
            properties = dependencies[requiringField]['properties'] || { };
          }

          // Validate property dependencies
          for (let requiredField of requiredFields) {
            if (xor(!hasValue(control.value[requiredField]), invert)) {
              requiringFieldErrors[requiredField] = { 'required': true };
            }
          }

          // Validate schema dependencies
          requiringFieldErrors = _mergeObjects(requiringFieldErrors,
            forEachCopy(properties, (requirements, requiredField) => {
              let requiredFieldErrors: PlainObject = _mergeObjects(
                forEachCopy(requirements, (requirement, parameter) => {
                  let validator: IValidatorFn = null;
                  if (requirement === 'maximum' || requirement === 'minimum') {
                    let exclusive: boolean =
                      !!requirements['exclusiveM' + requirement.slice(1)];
                    validator = JsonValidators[requirement](parameter, exclusive);
                  } else if (typeof JsonValidators[requirement] === 'function') {
                    validator = JsonValidators[requirement](parameter);
                  }
                  return !isDefined(validator) ?
                    null : validator(control.value[requiredField]);
                })
              );
              return isEmpty(requiredFieldErrors) ?
                null : { [requiredField]: requiredFieldErrors };
            })
          );
          return isEmpty(requiringFieldErrors) ?
            null : { [requiringField]: requiringFieldErrors };
        })
      );
      return isEmpty(allErrors) ? null : allErrors;
    };
  }

  /**
   * 'minItems' validator
   *
   * Requires a form array to have a minimum number of values.
   *
   * @param {number} minItems - minimum number of items allowed
   * @return {IValidatorFn}
   */
  static minItems(minItems: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let actualItems: number = isArray(control.value) ? control.value.length : 0;
      let isValid: boolean = actualItems >= minItems;
      return xor(isValid, invert) ?
        null : { 'minItems': { minItems, actualItems } };
    };
  }

  /**
   * 'maxItems' validator
   *
   * Requires a form array to have a maximum number of values.
   *
   * @param {number} maxItems - maximum number of items allowed
   * @return {IValidatorFn}
   */
  static maxItems(maxItems: number): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let actualItems: number = isArray(control.value) ? control.value.length : 0;
      let isValid: boolean = actualItems <= maxItems;
      return xor(isValid, invert) ?
        null : { 'maxItems': { maxItems, actualItems } };
    };
  }

  /**
   * 'uniqueItems' validator
   *
   * Requires values in a form array to be unique.
   *
   * @param {boolean = true} unique? - true to validate, false to disable
   * @return {IValidatorFn}
   */
  static uniqueItems(unique: boolean = true): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (!unique) { return null; }
      if (isEmpty(control.value)) { return null; }
      let sorted: any[] = control.value.slice().sort();
      let duplicateItems = [];
      for (let i = 1, l = sorted.length; i < l; i++) {
        if (sorted[i - 1] === sorted[i] &&
          duplicateItems.indexOf(sorted[i]) !== -1
        ) {
          duplicateItems.push(sorted[i]);
        }
      }
      let isValid: boolean = !duplicateItems.length;
      return xor(isValid, invert) ?
        null : { 'uniqueItems': { duplicateItems } };
    };
  }

  /**
   * No-op validator. Included for backward compatibility.
   */
  static nullValidator(c: AbstractControl): PlainObject { return null; }

  /**
   * Validator transformation functions:
   * composeAnyOf, composeOneOf, composeAllOf, composeNot,
   * compose, composeAsync
   *
   * TODO: Add composeAnyOfAsync, composeOneOfAsync,
   *   composeAllOfAsync, composeNotAsync
   */

  /**
   * 'composeAnyOf' validator combination function
   *
   * Accepts an array of validators and returns a single validator that
   * evaluates to valid if any one or more of the submitted validators are
   * valid. If every validator is invalid, it returns combined errors from
   * all validators.
   *
   * @param {IValidatorFn[]} validators - array of validators to combine
   * @return {IValidatorFn} - single combined validator function
   */
  static composeAnyOf(validators: IValidatorFn[]): IValidatorFn {
    if (!validators) { return null; }
    let presentValidators: IValidatorFn[] = validators.filter(isDefined);
    if (presentValidators.length === 0) { return null; }
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let arrayOfErrors: PlainObject[] =
        _executeValidators(control, presentValidators, invert).filter(isDefined);
      let isValid: boolean = validators.length > arrayOfErrors.length;
      return xor(isValid, invert) ?
        null : _mergeObjects.apply(arrayOfErrors.concat({ 'anyOf': !invert }));
    };
  }

  /**
   * 'composeOneOf' validator combination function
   *
   * Accepts an array of validators and returns a single validator that
   * evaluates to valid only if exactly one of the submitted validators
   * is valid. Otherwise returns combined information from all validators,
   * both valid and invalid.
   *
   * @param {IValidatorFn[]} validators - array of validators to combine
   * @return {IValidatorFn} - single combined validator function
   */
  static composeOneOf(validators: IValidatorFn[]): IValidatorFn {
    if (!validators) { return null; }
    let presentValidators: IValidatorFn[] = validators.filter(isDefined);
    if (presentValidators.length === 0) { return null; }
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let arrayOfErrors: PlainObject[] =
        _executeValidators(control, presentValidators);
      let validControls: number =
        validators.length - arrayOfErrors.filter(isDefined).length;
      let isValid: boolean = validControls === 1;
      if (xor(isValid, invert)) { return null; }
      let arrayOfValids: PlainObject[] =
        _executeValidators(control, presentValidators, invert);
      return _mergeObjects.apply(
        arrayOfErrors.concat(arrayOfValids).concat({ 'oneOf': !invert })
      );
    };
  }

  /**
   * 'composeAllOf' validator combination function
   *
   * Accepts an array of validators and returns a single validator that
   * evaluates to valid only if all the submitted validators are individually
   * valid. Otherwise it returns combined errors from all invalid validators.
   *
   * @param {IValidatorFn[]} validators - array of validators to combine
   * @return {IValidatorFn} - single combined validator function
   */
  static composeAllOf(validators: IValidatorFn[]): IValidatorFn {
    if (!validators) { return null; }
    let presentValidators: IValidatorFn[] = validators.filter(isDefined);
    if (presentValidators.length === 0) { return null; }
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      let combinedErrors = _mergeErrors(
        _executeValidators(control, presentValidators, invert)
      );
      let isValid: boolean = combinedErrors === null;
      return (xor(isValid, invert)) ?
        null : _mergeObjects(combinedErrors, { 'allOf': !invert });
    };
  }

  /**
   * 'composeNot' validator inversion function
   *
   * Accepts a single validator function and inverts its result.
   * Returns valid if the submitted validator is invalid, and
   * returns invalid if the submitted validator is valid.
   * (Note: this function can itself be inverted
   * - e.g. composeNot(composeNot(validator)) -
   * but this can be confusing and is therefore not recommended.)
   *
   * @param {IValidatorFn[]} validators - validator to invert
   * @return {IValidatorFn} - new validator function that returns opposite result
   */
  static composeNot(validator: IValidatorFn): IValidatorFn {
    return (control: AbstractControl, invert: boolean = false): PlainObject => {
      if (isEmpty(control.value)) { return null; }
      let error: PlainObject = validator(control, !invert);
      let isValid: boolean = error === null;
      return (xor(isValid, invert)) ?
        null : _mergeObjects(error, { 'not': !invert });
    };
  }

  /**
   * 'compose' validator combination function
   *
   * @param {IValidatorFn[]} validators - array of validators to combine
   * @return {IValidatorFn} - single combined validator function
   */
  static compose(validators: IValidatorFn[]): IValidatorFn {
    if (!validators) { return null; }
    let presentValidators = validators.filter(isDefined);
    if (presentValidators.length === 0) { return null; }
    return (control: AbstractControl, invert: boolean = false): PlainObject =>
      _mergeErrors(_executeValidators(control, presentValidators, invert));
  };

  /**
   * 'composeAsync' async validator combination function
   *
   * @param {AsyncIValidatorFn[]} async validators - array of async validators
   * @return {AsyncIValidatorFn} - single combined async validator function
   */
  static composeAsync(validators: AsyncIValidatorFn[]): AsyncIValidatorFn {
    if (!validators) { return null; }
    let presentValidators = validators.filter(isDefined);
    if (presentValidators.length === 0) { return null; }
    return (control: AbstractControl, invert: boolean = false) => Promise.all(
      _executeAsyncValidators(control, presentValidators).map(_convertToPromise)
    ).then(_mergeErrors);
  }
}
