/*!
 * ui-grid - v4.4.6 - 2018-04-06
 * Copyright (c) 2018 ; License: MIT 
 */

(function () {
  'use strict';
  
  /**
   * @ngdoc overview
   * @name ui.grid.validate
   * @description
   *
   * # ui.grid.validate
   *
   * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
   *
   * This module provides the ability to validate cells upon change.
   *
   * Design information:
   * -------------------
   *
   * Validation is not based on angularjs validation, since it would work only when editing the field.
   * 
   * Instead it adds custom properties to any field considered as invalid.
   *
   * <br/>
   * <br/>
   *
   * <div doc-module-components="ui.grid.expandable"></div>
   */

  var module = angular.module('ui.grid.validate', ['ui.grid']);
  
  
  /**
   *  @ngdoc service
   *  @name ui.grid.validate.service:uiGridValidateService
   *
   *  @description Services for validation features
   */
  module.service('uiGridValidateService', ['$sce', '$q', '$http', 'i18nService', 'uiGridConstants', function ($sce, $q, $http, i18nService, uiGridConstants) {

    var service = {
      
      /**
       *  @ngdoc object
       *  @name validatorFactories
       *  @propertyOf ui.grid.validate.service:uiGridValidateService
       *  @description object containing all the factories used to validate data.<br/>
       *  These factories will be in the form <br/>
       *  ```
       *  {
       *    validatorFactory: function(argument) {
       *                        return function(newValue, oldValue, rowEntity, colDef) {
       *                          return true || false || promise
       *                        }
       *                      },
       *    messageFunction: function(argument) {
       *                       return string
       *                     }
       *  }
       *  ```
       *
       * Promises should return true or false as result according to the result of validation.
       */
      validatorFactories: {},

      
      /**
       * @ngdoc service
       * @name setExternalFactoryFunction
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Adds a way to retrieve validators from an external service
       * <p>Validators from this external service have a higher priority than default
       * ones
       * @param {function} externalFactoryFunction a function that accepts name and argument to pass to a
       * validator factory and that returns an object with the same properties as 
       * you can see in {@link ui.grid.validate.service:uiGridValidateService#properties_validatorFactories validatorFactories}
       */
      setExternalFactoryFunction: function(externalFactoryFunction) {
        service.externalFactoryFunction = externalFactoryFunction;
      },
      
      /**
       * @ngdoc service
       * @name clearExternalFactory
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Removes any link to external factory from this service
       */
      clearExternalFactory: function() {
        delete service.externalFactoryFunction;
      },

      /**
       * @ngdoc service
       * @name getValidatorFromExternalFactory
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Retrieves a validator by executing a validatorFactory
       * stored in an external service.
       * @param {string} name the name of the validator to retrieve
       * @param {object} argument an argument to pass to the validator factory
       */
      getValidatorFromExternalFactory: function(name, argument) {
        return service.externalFactoryFunction(name, argument).validatorFactory(argument);
      },
      
      /**
       * @ngdoc service
       * @name getMessageFromExternalFactory
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Retrieves a message stored in an external service.
       * @param {string} name the name of the validator
       * @param {object} argument an argument to pass to the message function
       */
      getMessageFromExternalFactory: function(name, argument) {
        return service.externalFactoryFunction(name, argument).messageFunction(argument);
      },
      
      /**
       * @ngdoc service
       * @name setValidator
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description  Adds a new validator to the service
       * @param {string} name the name of the validator, must be unique
       * @param {function} validatorFactory a factory that return a validatorFunction
       * @param {function} messageFunction a function that return the error message
       */
      setValidator: function(name, validatorFactory, messageFunction) {
        service.validatorFactories[name] = {
          validatorFactory: validatorFactory,
          messageFunction: messageFunction
        };
      },

      /**
       * @ngdoc service
       * @name getValidator
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Returns a validator registered to the service
       * or retrieved from the external factory
       * @param {string} name the name of the validator to retrieve
       * @param {object} argument an argument to pass to the validator factory
       * @returns {object} the validator function
       */
      getValidator: function(name, argument) {
        if (service.externalFactoryFunction) {
          var validator = service.getValidatorFromExternalFactory(name, argument);
          if (validator) {
            return validator;
          }
        }
        if (!service.validatorFactories[name]) {
          throw ("Invalid validator name: " + name);
        }
        return service.validatorFactories[name].validatorFactory(argument);
      },

      /**
       * @ngdoc service
       * @name getMessage
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Returns the error message related to the validator 
       * @param {string} name the name of the validator
       * @param {object} argument an argument to pass to the message function
       * @returns {string} the error message related to the validator
       */
      getMessage: function(name, argument) {
        if (service.externalFactoryFunction) {
          var message = service.getMessageFromExternalFactory(name, argument);
          if (message) {
            return message;
          }
        }
        return service.validatorFactories[name].messageFunction(argument);
      },

      /**
       * @ngdoc service
       * @name isInvalid
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Returns true if the cell (identified by rowEntity, colDef) is invalid 
       * @param {object} rowEntity the row entity of the cell
       * @param {object} colDef the colDef of the cell
       * @returns {boolean} true if the cell is invalid
       */
      isInvalid: function (rowEntity, colDef) {
        return rowEntity['$$invalid'+colDef.name];
      },

      /**
       * @ngdoc service
       * @name setInvalid
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Makes the cell invalid by adding the proper field to the entity
       * @param {object} rowEntity the row entity of the cell
       * @param {object} colDef the colDef of the cell
       */
      setInvalid: function (rowEntity, colDef) {
        rowEntity['$$invalid'+colDef.name] = true;
      },
    
      /**
       * @ngdoc service
       * @name setValid
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Makes the cell valid by removing the proper error field from the entity
       * @param {object} rowEntity the row entity of the cell
       * @param {object} colDef the colDef of the cell
       */
      setValid: function (rowEntity, colDef) {
        delete rowEntity['$$invalid'+colDef.name];
      },

      /**
       * @ngdoc service
       * @name setError
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Adds the proper error to the entity errors field
       * @param {object} rowEntity the row entity of the cell
       * @param {object} colDef the colDef of the cell
       * @param {string} validatorName the name of the validator that is failing
       */
      setError: function(rowEntity, colDef, validatorName) {
        if (!rowEntity['$$errors'+colDef.name]) {
          rowEntity['$$errors'+colDef.name] = {};
        }
        rowEntity['$$errors'+colDef.name][validatorName] = true;
      },

      /**
       * @ngdoc service
       * @name clearError
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Removes the proper error from the entity errors field
       * @param {object} rowEntity the row entity of the cell
       * @param {object} colDef the colDef of the cell
       * @param {string} validatorName the name of the validator that is failing
       */
      clearError: function(rowEntity, colDef, validatorName) {
        if (!rowEntity['$$errors'+colDef.name]) {
          return;
        }
        if (validatorName in rowEntity['$$errors'+colDef.name]) {
            delete rowEntity['$$errors'+colDef.name][validatorName];
        }
      },
      
      /**
       * @ngdoc function
       * @name getErrorMessages
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description returns an array of i18n-ed error messages.
       * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
       * @param {object} colDef the column whose errors we are looking for
       * @returns {array} An array of strings containing all the error messages for the cell
       */
      getErrorMessages: function(rowEntity, colDef) {
        var errors = [];

        if (!rowEntity['$$errors'+colDef.name] || Object.keys(rowEntity['$$errors'+colDef.name]).length === 0) {
          return errors;
        }

        Object.keys(rowEntity['$$errors'+colDef.name]).sort().forEach(function(validatorName) {
          errors.push(service.getMessage(validatorName, colDef.validators[validatorName]));
        });
        
        return errors;
      },
      
      /**
       * @ngdoc function
       * @name getFormattedErrors
       * @methodOf  ui.grid.validate.service:uiGridValidateService
       * @description returns the error i18n-ed and formatted in html to be shown inside the page.
       * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
       * @param {object} colDef the column whose errors we are looking for
       * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
       * message inside the page (i.e. inside a div)
       */
      getFormattedErrors: function(rowEntity, colDef) {

        var msgString = "";

        var errors = service.getErrorMessages(rowEntity, colDef);
        
        if (!errors.length) {
          return;
        }
        
        errors.forEach(function(errorMsg) {
          msgString += errorMsg + "<br/>";
        });

        return $sce.trustAsHtml('<p><b>' + i18nService.getSafeText('validate.error') + '</b></p>' + msgString );
      },

      /**
       * @ngdoc function
       * @name getTitleFormattedErrors
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html 
       * title attribute.
       * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
       * @param {object} colDef the column whose errors we are looking for
       * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
       * message inside an html title attribute
       */
      getTitleFormattedErrors: function(rowEntity, colDef) {

        var newLine = "\n";

        var msgString = "";
        
        var errors = service.getErrorMessages(rowEntity, colDef);
        
        if (!errors.length) {
          return;
        }
        
        errors.forEach(function(errorMsg) {
          msgString += errorMsg + newLine;
        });

        return $sce.trustAsHtml(i18nService.getSafeText('validate.error') + newLine + msgString);
      },

      /**
       * @ngdoc function
       * @name getTitleFormattedErrors
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description Executes all validators on a cell (identified by row entity and column definition) and sets or clears errors
       * @param {object} rowEntity the row entity of the cell we want to run the validators on
       * @param {object} colDef the column definition of the cell we want to run the validators on
       * @param {object} newValue the value the user just entered
       * @param {object} oldValue the value the field had before
       */
      runValidators: function(rowEntity, colDef, newValue, oldValue, grid) {
        
        if (newValue === oldValue) {
          // If the value has not changed we perform no validation
          return;
        }
        
        if (typeof(colDef.name) === 'undefined' || !colDef.name) {
          throw new Error('colDef.name is required to perform validation');
        }
        
        service.setValid(rowEntity, colDef);
        
        var validateClosureFactory = function(rowEntity, colDef, validatorName) {
          return function(value) {
            if (!value) {
              service.setInvalid(rowEntity, colDef);
              service.setError(rowEntity, colDef, validatorName);
              if (grid) {
                grid.api.validate.raise.validationFailed(rowEntity, colDef, newValue, oldValue);
              }
            }
          };
        };

        var promises = [];

        for (var validatorName in colDef.validators) {
          service.clearError(rowEntity, colDef, validatorName);
          var msg;
          var validatorFunction = service.getValidator(validatorName, colDef.validators[validatorName]);
          // We pass the arguments as oldValue, newValue so they are in the same order 
          // as ng-model validators (modelValue, viewValue)
          var promise = $q
                        .when(validatorFunction(oldValue, newValue, rowEntity, colDef))
                        .then(validateClosureFactory(rowEntity, colDef, validatorName));
          promises.push(promise);
        }
        
        return $q.all(promises);
      },

      /**
       * @ngdoc function
       * @name createDefaultValidators
       * @methodOf ui.grid.validate.service:uiGridValidateService
       * @description adds the basic validators to the list of service validators
       */
      createDefaultValidators: function() {
        service.setValidator('minLength',
                             function (argument) {
                               return function (oldValue, newValue, rowEntity, colDef) {
                                 if (newValue === undefined || newValue === null || newValue === '') {
                                   return true;
                                 }
                                 return newValue.length >= argument;
                               };
                             },
                               function(argument) {
                                 return i18nService.getSafeText('validate.minLength').replace('THRESHOLD', argument);
                               });
        
        service.setValidator('maxLength',
                             function (argument) {
                               return function (oldValue, newValue, rowEntity, colDef) {
                                 if (newValue === undefined || newValue === null || newValue === '') {
                                   return true;
                                 }
                                 return newValue.length <= argument;
                               };
                             },
                             function(threshold) {
                               return i18nService.getSafeText('validate.maxLength').replace('THRESHOLD', threshold);
                             });
        
        service.setValidator('required',
                             function (argument) {
                               return function (oldValue, newValue, rowEntity, colDef) {
                                 if (argument) {
                                   return !(newValue === undefined || newValue === null || newValue === '');
                                 }
                                 return true;
                               };
                             },
                             function(argument) {
                               return i18nService.getSafeText('validate.required');
                             });
      },

      initializeGrid: function (scope, grid) {
        grid.validate = {
        
          isInvalid: service.isInvalid,

          getFormattedErrors: service.getFormattedErrors,
         
          getTitleFormattedErrors: service.getTitleFormattedErrors,

          runValidators: service.runValidators
        };
        
        /**
         *  @ngdoc object
         *  @name ui.grid.validate.api:PublicApi
         *
         *  @description Public Api for validation feature
         */
        var publicApi = {
          events: {
            validate: {
              /**
               * @ngdoc event
               * @name validationFailed
               * @eventOf  ui.grid.validate.api:PublicApi
               * @description raised when one or more failure happened during validation 
               * <pre>
               *      gridApi.validate.on.validationFailed(scope, function(rowEntity, colDef, newValue, oldValue){...})
               * </pre>
               * @param {object} rowEntity the options.data element whose validation failed
               * @param {object} colDef the column whose validation failed
               * @param {object} newValue new value
               * @param {object} oldValue old value
               */
              validationFailed: function (rowEntity, colDef, newValue, oldValue) {
              }
            }
          },
          methods: {
            validate: {
              /**
               * @ngdoc function
               * @name isInvalid
               * @methodOf  ui.grid.validate.api:PublicApi
               * @description checks if a cell (identified by rowEntity, colDef) is invalid
               * @param {object} rowEntity gridOptions.data[] array instance we want to check
               * @param {object} colDef the column whose errors we want to check
               * @returns {boolean} true if the cell value is not valid
               */
              isInvalid: function(rowEntity, colDef) {
                return grid.validate.isInvalid(rowEntity, colDef);
              },
              /**
               * @ngdoc function
               * @name getErrorMessages
               * @methodOf  ui.grid.validate.api:PublicApi
               * @description returns an array of i18n-ed error messages.
               * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
               * @param {object} colDef the column whose errors we are looking for
               * @returns {array} An array of strings containing all the error messages for the cell
               */
              getErrorMessages: function (rowEntity, colDef) {
                return grid.validate.getErrorMessages(rowEntity, colDef);
              },
              /**
               * @ngdoc function
               * @name getFormattedErrors
               * @methodOf  ui.grid.validate.api:PublicApi
               * @description returns the error i18n-ed and formatted in html to be shown inside the page.
               * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
               * @param {object} colDef the column whose errors we are looking for
               * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
               * message inside the page (i.e. inside a div)
               */
              getFormattedErrors: function (rowEntity, colDef) {
                return grid.validate.getFormattedErrors(rowEntity, colDef);
              },
              /**
               * @ngdoc function
               * @name getTitleFormattedErrors
               * @methodOf  ui.grid.validate.api:PublicApi
               * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html 
               * title attribute.
               * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
               * @param {object} colDef the column whose errors we are looking for
               * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
               * message inside an html title attribute
               */
              getTitleFormattedErrors: function (rowEntity, colDef) {
                return grid.validate.getTitleFormattedErrors(rowEntity, colDef);
              }
            } 
          }
        };
        
        grid.api.registerEventsFromObject(publicApi.events);
        grid.api.registerMethodsFromObject(publicApi.methods);

        if (grid.edit) {
          grid.api.edit.on.afterCellEdit(scope, function(rowEntity, colDef, newValue, oldValue) {
            grid.validate.runValidators(rowEntity, colDef, newValue, oldValue, grid);
          });
        }

        service.createDefaultValidators();
      }
      
    };
  
    return service;
  }]);
  
  
  /**
   *  @ngdoc directive
   *  @name ui.grid.validate.directive:uiGridValidate
   *  @element div
   *  @restrict A
   *  @description Adds validating features to the ui-grid directive.
   *  @example
   <example module="app">
   <file name="app.js">
   var app = angular.module('app', ['ui.grid', 'ui.grid.edit', 'ui.grid.validate']);

   app.controller('MainCtrl', ['$scope', function ($scope) {
      $scope.data = [
        { name: 'Bob', title: 'CEO' },
            { name: 'Frank', title: 'Lowly Developer' }
      ];

      $scope.columnDefs = [
        {name: 'name', enableCellEdit: true, validators: {minLength: 3, maxLength: 9}, cellTemplate: 'ui-grid/cellTitleValidator'},
        {name: 'title', enableCellEdit: true, validators: {required: true}, cellTemplate: 'ui-grid/cellTitleValidator'}
      ];
    }]);
   </file>
   <file name="index.html">
   <div ng-controller="MainCtrl">
   <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit ui-grid-validate></div>
   </div>
   </file>
   </example>
   */

  module.directive('uiGridValidate', ['gridUtil', 'uiGridValidateService', function (gridUtil, uiGridValidateService) {
    return {
      priority: 0,
      replace: true,
      require: '^uiGrid',
      scope: false,
      compile: function () {
        return {
          pre: function ($scope, $elm, $attrs, uiGridCtrl) {
            uiGridValidateService.initializeGrid($scope, uiGridCtrl.grid);
          },
          post: function ($scope, $elm, $attrs, uiGridCtrl) {
          }
        };
      }
    };
  }]);
})();
