/**
 * Utility methods and properties attached to $.formUtils
 */
(function($, window) {

  'use strict';

  var $win = $(window);

  $.formUtils = $.extend($.formUtils || {}, {

    $win: $win,

    /**
     * Default config for $(...).isValid();
     */
    defaultConfig: function () {
      return {
        ignore: [], // Names of inputs not to be validated even though `validationRuleAttribute` containing the validation rules tells us to
        errorElementClass: 'error', // Class that will be put on elements which value is invalid
        successElementClass: 'valid', // Class that will be put on elements that has been validated with success
        borderColorOnError: '#b94a48', // Border color of elements which value is invalid, empty string to not change border color
        errorMessageClass: 'form-error', // class name of div containing error messages when validation fails
        validationRuleAttribute: 'data-validation', // name of the attribute holding the validation rules
        validationErrorMsgAttribute: 'data-validation-error-msg', // define custom err msg inline with element
        errorMessagePosition: 'inline', // Can be either "top" or "inline"
        errorMessageTemplate: {
          container: '<div class="{errorMessageClass} alert alert-danger">{messages}</div>',
          messages: '<strong>{errorTitle}</strong><ul>{fields}</ul>',
          field: '<li>{msg}</li>'
        },
        scrollToTopOnError: true,
        dateFormat: 'yyyy-mm-dd',
        addValidClassOnAll: false, // whether or not to apply class="valid" even if the input wasn't validated
        decimalSeparator: '.',
        inputParentClassOnError: 'has-error', // twitter-bootstrap default class name
        inputParentClassOnSuccess: 'has-success', // twitter-bootstrap default class name
        validateHiddenInputs: false, // whether or not hidden inputs should be validated
        inlineErrorMessageCallback: false,
        submitErrorMessageCallback: false
      };
    },

    /**
     * Available validators
     */
    validators: {},

    /**
     * Events triggered by form validator
     */
    _events: {load: [], valid: [], invalid: []},

    /**
     * Setting this property to true during validation will
     * stop further validation from taking place and form will
     * not be sent
     */
    haltValidation: false,

    /**
     * Function for adding a validator
     * @see $.formUtils.addAsyncValidator (async.js)
     * @param {Object} validator
     */
    addValidator: function (validator) {
      // prefix with "validate_" for backward compatibility reasons
      var name = validator.name.indexOf('validate_') === 0 ? validator.name : 'validate_' + validator.name;
      if (validator.validateOnKeyUp === undefined) {
        validator.validateOnKeyUp = true;
      }
      this.validators[name] = validator;
    },

    /**
     * Warn user via the console if available
     */
    warn: function(msg, fallbackOnAlert) {
      if( 'console' in window ) {
        if( typeof window.console.warn === 'function' ) {
          window.console.warn(msg);
        } else if( typeof window.console.log === 'function' ) {
          window.console.log(msg);
        }
      } else if (fallbackOnAlert) {
        // This is for some old IE version...
        alert(msg);
      }
    },

    /**
     * Same as input $.fn.val() but also supporting input of typ radio or checkbox
     * @example
     *
     *  $.formUtils.getValue('.myRadioButtons', $('#some-form'));
     *  $.formUtils.getValue($('#some-form').find('.check-boxes'));
     *
     * @param query
     * @param $parent
     * @returns {String|Boolean}
     */
    getValue: function(query, $parent) {
      var $inputs = $parent ? $parent.find(query) : query;
      if ($inputs.length > 0 ) {
        var type = $inputs.eq(0).attr('type');
        if (type === 'radio' || type === 'checkbox') {
          return $inputs.filter(':checked').val() || '';
        } else {
          return $inputs.val() || '';
        }
      }
      return false;
    },

    /**
     * Validate the value of given element according to the validation rules
     * found in the attribute data-validation. Will return an object representing
     * a validation result, having the props shouldChangeDisplay, isValid and errorMsg
     * @param {jQuery} $elem
     * @param {Object} language ($.formUtils.LANG)
     * @param {Object} conf
     * @param {jQuery} $form
     * @param {String} [eventContext]
     * @return {Object}
     */
    validateInput: function ($elem, language, conf, $form, eventContext) {

      conf = conf || $.formUtils.defaultConfig();
      language = language || $.formUtils.LANG;

      if (!$form.length) {
        $form = $elem.parent();
      }

      var value = this.getValue($elem);

      $elem
        .valAttr('skipped', false)
        .one('beforeValidation', function() {
          // Skip input because its hidden or disabled
          // Doing this in a callback makes it possible for others to prevent the default
          // behaviour by binding to the same event and call evt.stopImmediatePropagation()
          if ($elem.attr('disabled') || (!$elem.is(':visible') && !conf.validateHiddenInputs)) {
            $elem.valAttr('skipped', 1);
          }
        })
        .trigger('beforeValidation', [value, language, conf]);

      var inputIsOptional = $elem.valAttr('optional') === 'true',
          skipBecauseItsEmpty = !value && inputIsOptional,
          validationRules = $elem.attr(conf.validationRuleAttribute),
          isValid = true,
          errorMsg = '',
          result = {isValid: true, shouldChangeDisplay:true, errorMsg:''};

      // For input type="number", browsers attempt to parse the entered value into a number.
      // If the input is not numeric, browsers handle the situation differently:
      // Chrome 48 simply disallows non-numeric input; FF 44 clears out the input box on blur;
      // Safari 5 parses the entered string to find a leading number.
      // If the input fails browser validation, the browser sets the input value equal to an empty string.
      // Therefore, we cannot distinguish (apart from hacks) between an empty input type="text" and one with a
      // value that can't be parsed by the browser.

      if (!validationRules || skipBecauseItsEmpty || $elem.valAttr('skipped')) {
        result.shouldChangeDisplay = conf.addValidClassOnAll;
        return result;
      }

      // Filter out specified characters
      var ignore = $elem.valAttr('ignore');
      if (ignore) {
        $.each(ignore.split(''), function(i, character) {
          value = value.replace(new RegExp('\\'+character, 'g'), '');
        });
      }

      $.split(validationRules, function (rule) {

        if (rule.indexOf('validate_') !== 0) {
          rule = 'validate_' + rule;
        }

        var validator = $.formUtils.validators[rule];

        if (validator) {

          // special change of element for checkbox_group rule
          if (rule === 'validate_checkbox_group') {
            // set element to first in group, so error msg attr doesn't need to be set on all elements in group
            $elem = $form.find('[name="' + $elem.attr('name') + '"]:eq(0)');
          }

          if (eventContext !== 'keyup' || validator.validateOnKeyUp) {
            // A validator can prevent itself from getting triggered on keyup
            isValid = validator.validatorFunction(value, $elem, conf, language, $form, eventContext);
          }

          if (!isValid) {
            if (conf.validateOnBlur) {
              $elem.validateOnKeyUp(language, conf);
            }
            errorMsg = $.formUtils.dialogs.resolveErrorMessage($elem, validator, rule, conf, language);
            return false; // break iteration
          }

        } else {

          // todo: Add some validator lookup function and tell immediately which module is missing
          throw new Error('Using undefined validator "' + rule +
            '". Maybe you have forgotten to load the module that "' + rule +'" belongs to?');

        }

      });


      if (isValid === false) {
        $elem.trigger('validation', false);
        result.errorMsg = errorMsg;
        result.isValid = false;
        result.shouldChangeDisplay = true;
      } else if (isValid === null) {
        // A validatorFunction returning null means that it's not able to validate
        // the input at this time. Most probably some async stuff need to gets finished
        // first and then the validator will re-trigger the validation.
        result.shouldChangeDisplay = false;
      } else {
        $elem.trigger('validation', true);
        result.shouldChangeDisplay = true;
      }

      // Run element validation callback
      if (typeof conf.onElementValidate === 'function' && errorMsg !== null) {
        conf.onElementValidate(result.isValid, $elem, $form, errorMsg);
      }

      $elem.trigger('afterValidation', [result, eventContext]);

      return result;
    },

    /**
     * Is it a correct date according to given dateFormat. Will return false if not, otherwise
     * an array 0=>year 1=>month 2=>day
     *
     * @param {String} val
     * @param {String} dateFormat
     * @param {Boolean} [addMissingLeadingZeros]
     * @return {Array}|{Boolean}
     */
    parseDate: function (val, dateFormat, addMissingLeadingZeros) {
      var divider = dateFormat.replace(/[a-zA-Z]/gi, '').substring(0, 1),
        regexp = '^',
        formatParts = dateFormat.split(divider || null),
        matches, day, month, year;

      $.each(formatParts, function (i, part) {
        regexp += (i > 0 ? '\\' + divider : '') + '(\\d{' + part.length + '})';
      });

      regexp += '$';

      if (addMissingLeadingZeros) {
        var newValueParts = [];
        $.each(val.split(divider), function(i, part) {
          if(part.length === 1) {
            part = '0'+part;
          }
          newValueParts.push(part);
        });
        val = newValueParts.join(divider);
      }

      matches = val.match(new RegExp(regexp));
      if (matches === null) {
        return false;
      }

      var findDateUnit = function (unit, formatParts, matches) {
        for (var i = 0; i < formatParts.length; i++) {
          if (formatParts[i].substring(0, 1) === unit) {
            return $.formUtils.parseDateInt(matches[i + 1]);
          }
        }
        return -1;
      };

      month = findDateUnit('m', formatParts, matches);
      day = findDateUnit('d', formatParts, matches);
      year = findDateUnit('y', formatParts, matches);

      if ((month === 2 && day > 28 && (year % 4 !== 0 || year % 100 === 0 && year % 400 !== 0)) ||
        (month === 2 && day > 29 && (year % 4 === 0 || year % 100 !== 0 && year % 400 === 0)) ||
        month > 12 || month === 0) {
        return false;
      }
      if ((this.isShortMonth(month) && day > 30) || (!this.isShortMonth(month) && day > 31) || day === 0) {
        return false;
      }

      return [year, month, day];
    },

    /**
     * skum fix. är talet 05 eller lägre ger parseInt rätt int annars får man 0 när man kör parseInt?
     *
     * @param {String} val
     * @return {Number}
     */
    parseDateInt: function (val) {
      if (val.indexOf('0') === 0) {
        val = val.replace('0', '');
      }
      return parseInt(val, 10);
    },

    /**
     * Has month only 30 days?
     *
     * @param {Number} m
     * @return {Boolean}
     */
    isShortMonth: function (m) {
      return (m % 2 === 0 && m < 7) || (m % 2 !== 0 && m > 7);
    },

    /**
     * Restrict input length
     *
     * @param {jQuery} $inputElement Jquery Html object
     * @param {jQuery} $maxLengthElement jQuery Html Object
     * @return void
     */
    lengthRestriction: function ($inputElement, $maxLengthElement) {
      // read maxChars from counter display initial text value
      var maxChars = parseInt($maxLengthElement.text(), 10),
        charsLeft = 0,

      // internal function does the counting and sets display value
        countCharacters = function () {
          var numChars = $inputElement.val().length;
          if (numChars > maxChars) {
            // get current scroll bar position
            var currScrollTopPos = $inputElement.scrollTop();
            // trim value to max length
            $inputElement.val($inputElement.val().substring(0, maxChars));
            $inputElement.scrollTop(currScrollTopPos);
          }
          charsLeft = maxChars - numChars;
          if (charsLeft < 0) {
            charsLeft = 0;
          }

          // set counter text
          $maxLengthElement.text(charsLeft);
        };

      // bind events to this element
      // setTimeout is needed, cut or paste fires before val is available
      $($inputElement).bind('keydown keyup keypress focus blur', countCharacters)
        .bind('cut paste', function () {
          setTimeout(countCharacters, 100);
        });

      // count chars on pageload, if there are prefilled input-values
      $(document).bind('ready', countCharacters);
    },

    /**
     * Test numeric against allowed range
     *
     * @param $value int
     * @param $rangeAllowed str; (1-2, min1, max2, 10)
     * @return array
     */
    numericRangeCheck: function (value, rangeAllowed) {
      // split by dash
      var range = $.split(rangeAllowed),
      // min or max
        minmax = parseInt(rangeAllowed.substr(3), 10);

      if( range.length === 1 && rangeAllowed.indexOf('min') === -1 && rangeAllowed.indexOf('max') === -1 ) {
        range = [rangeAllowed, rangeAllowed]; // only a number, checking agains an exact number of characters
      }

      // range ?
      if (range.length === 2 && (value < parseInt(range[0], 10) || value > parseInt(range[1], 10) )) {
        return [ 'out', range[0], range[1] ];
      } // value is out of range
      else if (rangeAllowed.indexOf('min') === 0 && (value < minmax )) // min
      {
        return ['min', minmax];
      } // value is below min
      else if (rangeAllowed.indexOf('max') === 0 && (value > minmax )) // max
      {
        return ['max', minmax];
      } // value is above max
      // since no other returns executed, value is in allowed range
      return [ 'ok' ];
    },


    _numSuggestionElements: 0,
    _selectedSuggestion: null,
    _previousTypedVal: null,

    /**
     * Utility function that can be used to create plugins that gives
     * suggestions when inputs is typed into
     * @param {jQuery} $elem
     * @param {Array} suggestions
     * @param {Object} settings - Optional
     * @return {jQuery}
     */
    suggest: function ($elem, suggestions, settings) {
      var conf = {
          css: {
            maxHeight: '150px',
            background: '#FFF',
            lineHeight: '150%',
            textDecoration: 'underline',
            overflowX: 'hidden',
            overflowY: 'auto',
            border: '#CCC solid 1px',
            borderTop: 'none',
            cursor: 'pointer'
          },
          activeSuggestionCSS: {
            background: '#E9E9E9'
          }
        },
        setSuggsetionPosition = function ($suggestionContainer, $input) {
          var offset = $input.offset();
          $suggestionContainer.css({
            width: $input.outerWidth(),
            left: offset.left + 'px',
            top: (offset.top + $input.outerHeight()) + 'px'
          });
        };

      if (settings) {
        $.extend(conf, settings);
      }

      conf.css.position = 'absolute';
      conf.css['z-index'] = 9999;
      $elem.attr('autocomplete', 'off');

      if (this._numSuggestionElements === 0) {
        // Re-position suggestion container if window size changes
        $win.bind('resize', function () {
          $('.jquery-form-suggestions').each(function () {
            var $container = $(this),
              suggestID = $container.attr('data-suggest-container');
            setSuggsetionPosition($container, $('.suggestions-' + suggestID).eq(0));
          });
        });
      }

      this._numSuggestionElements++;

      var onSelectSuggestion = function ($el) {
        var suggestionId = $el.valAttr('suggestion-nr');
        $.formUtils._selectedSuggestion = null;
        $.formUtils._previousTypedVal = null;
        $('.jquery-form-suggestion-' + suggestionId).fadeOut('fast');
      };

      $elem
        .data('suggestions', suggestions)
        .valAttr('suggestion-nr', this._numSuggestionElements)
        .unbind('focus.suggest')
        .bind('focus.suggest', function () {
          $(this).trigger('keyup');
          $.formUtils._selectedSuggestion = null;
        })
        .unbind('keyup.suggest')
        .bind('keyup.suggest', function () {
          var $input = $(this),
            foundSuggestions = [],
            val = $.trim($input.val()).toLocaleLowerCase();

          if (val === $.formUtils._previousTypedVal) {
            return;
          }
          else {
            $.formUtils._previousTypedVal = val;
          }

          var hasTypedSuggestion = false,
            suggestionId = $input.valAttr('suggestion-nr'),
            $suggestionContainer = $('.jquery-form-suggestion-' + suggestionId);

          $suggestionContainer.scrollTop(0);

          // Find the right suggestions
          if (val !== '') {
            var findPartial = val.length > 2;
            $.each($input.data('suggestions'), function (i, suggestion) {
              var lowerCaseVal = suggestion.toLocaleLowerCase();
              if (lowerCaseVal === val) {
                foundSuggestions.push('<strong>' + suggestion + '</strong>');
                hasTypedSuggestion = true;
                return false;
              } else if (lowerCaseVal.indexOf(val) === 0 || (findPartial && lowerCaseVal.indexOf(val) > -1)) {
                foundSuggestions.push(suggestion.replace(new RegExp(val, 'gi'), '<strong>$&</strong>'));
              }
            });
          }

          // Hide suggestion container
          if (hasTypedSuggestion || (foundSuggestions.length === 0 && $suggestionContainer.length > 0)) {
            $suggestionContainer.hide();
          }

          // Create suggestion container if not already exists
          else if (foundSuggestions.length > 0 && $suggestionContainer.length === 0) {
            $suggestionContainer = $('<div></div>').css(conf.css).appendTo('body');
            $elem.addClass('suggestions-' + suggestionId);
            $suggestionContainer
              .attr('data-suggest-container', suggestionId)
              .addClass('jquery-form-suggestions')
              .addClass('jquery-form-suggestion-' + suggestionId);
          }

          // Show hidden container
          else if (foundSuggestions.length > 0 && !$suggestionContainer.is(':visible')) {
            $suggestionContainer.show();
          }

          // add suggestions
          if (foundSuggestions.length > 0 && val.length !== foundSuggestions[0].length) {

            // put container in place every time, just in case
            setSuggsetionPosition($suggestionContainer, $input);

            // Add suggestions HTML to container
            $suggestionContainer.html('');
            $.each(foundSuggestions, function (i, text) {
              $('<div></div>')
                .append(text)
                .css({
                  overflow: 'hidden',
                  textOverflow: 'ellipsis',
                  whiteSpace: 'nowrap',
                  padding: '5px'
                })
                .addClass('form-suggest-element')
                .appendTo($suggestionContainer)
                .click(function () {
                  $input.focus();
                  $input.val($(this).text());
                  $input.trigger('change');
                  onSelectSuggestion($input);
                });
            });
          }
        })
        .unbind('keydown.validation')
        .bind('keydown.validation', function (e) {
          var code = (e.keyCode ? e.keyCode : e.which),
            suggestionId,
            $suggestionContainer,
            $input = $(this);

          if (code === 13 && $.formUtils._selectedSuggestion !== null) {
            suggestionId = $input.valAttr('suggestion-nr');
            $suggestionContainer = $('.jquery-form-suggestion-' + suggestionId);
            if ($suggestionContainer.length > 0) {
              var newText = $suggestionContainer.find('div').eq($.formUtils._selectedSuggestion).text();
              $input.val(newText);
              $input.trigger('change');
              onSelectSuggestion($input);
              e.preventDefault();
            }
          }
          else {
            suggestionId = $input.valAttr('suggestion-nr');
            $suggestionContainer = $('.jquery-form-suggestion-' + suggestionId);
            var $suggestions = $suggestionContainer.children();
            if ($suggestions.length > 0 && $.inArray(code, [38, 40]) > -1) {
              if (code === 38) { // key up
                if ($.formUtils._selectedSuggestion === null) {
                  $.formUtils._selectedSuggestion = $suggestions.length - 1;
                }
                else{
                  $.formUtils._selectedSuggestion--;
                }
                if ($.formUtils._selectedSuggestion < 0) {
                  $.formUtils._selectedSuggestion = $suggestions.length - 1;
                }
              }
              else if (code === 40) { // key down
                if ($.formUtils._selectedSuggestion === null) {
                  $.formUtils._selectedSuggestion = 0;
                }
                else {
                  $.formUtils._selectedSuggestion++;
                }
                if ($.formUtils._selectedSuggestion > ($suggestions.length - 1)) {
                  $.formUtils._selectedSuggestion = 0;
                }
              }

              // Scroll in suggestion window
              var containerInnerHeight = $suggestionContainer.innerHeight(),
                containerScrollTop = $suggestionContainer.scrollTop(),
                suggestionHeight = $suggestionContainer.children().eq(0).outerHeight(),
                activeSuggestionPosY = suggestionHeight * ($.formUtils._selectedSuggestion);

              if (activeSuggestionPosY < containerScrollTop || activeSuggestionPosY > (containerScrollTop + containerInnerHeight)) {
                $suggestionContainer.scrollTop(activeSuggestionPosY);
              }

              $suggestions
                .removeClass('active-suggestion')
                .css('background', 'none')
                .eq($.formUtils._selectedSuggestion)
                .addClass('active-suggestion')
                .css(conf.activeSuggestionCSS);

              e.preventDefault();
              return false;
            }
          }
        })
        .unbind('blur.suggest')
        .bind('blur.suggest', function () {
          onSelectSuggestion($(this));
        });

      return $elem;
    },

    /**
     * Error dialogs
     *
     * @var {Object}
     */
    LANG: {
      errorTitle: 'Form submission failed!',
      requiredField: 'This is a required field',
      requiredFields: 'You have not answered all required fields',
      badTime: 'You have not given a correct time',
      badEmail: 'You have not given a correct e-mail address',
      badTelephone: 'You have not given a correct phone number',
      badSecurityAnswer: 'You have not given a correct answer to the security question',
      badDate: 'You have not given a correct date',
      lengthBadStart: 'The input value must be between ',
      lengthBadEnd: ' characters',
      lengthTooLongStart: 'The input value is longer than ',
      lengthTooShortStart: 'The input value is shorter than ',
      notConfirmed: 'Input values could not be confirmed',
      badDomain: 'Incorrect domain value',
      badUrl: 'The input value is not a correct URL',
      badCustomVal: 'The input value is incorrect',
      andSpaces: ' and spaces ',
      badInt: 'The input value was not a correct number',
      badSecurityNumber: 'Your social security number was incorrect',
      badUKVatAnswer: 'Incorrect UK VAT Number',
      badUKNin: 'Incorrect UK NIN',
      badUKUtr: 'Incorrect UK UTR Number',
      badStrength: 'The password isn\'t strong enough',
      badNumberOfSelectedOptionsStart: 'You have to choose at least ',
      badNumberOfSelectedOptionsEnd: ' answers',
      badAlphaNumeric: 'The input value can only contain alphanumeric characters ',
      badAlphaNumericExtra: ' and ',
      wrongFileSize: 'The file you are trying to upload is too large (max %s)',
      wrongFileType: 'Only files of type %s is allowed',
      groupCheckedRangeStart: 'Please choose between ',
      groupCheckedTooFewStart: 'Please choose at least ',
      groupCheckedTooManyStart: 'Please choose a maximum of ',
      groupCheckedEnd: ' item(s)',
      badCreditCard: 'The credit card number is not correct',
      badCVV: 'The CVV number was not correct',
      wrongFileDim : 'Incorrect image dimensions,',
      imageTooTall : 'the image can not be taller than',
      imageTooWide : 'the image can not be wider than',
      imageTooSmall : 'the image was too small',
      min : 'min',
      max : 'max',
      imageRatioNotAccepted : 'Image ratio is not be accepted',
      badBrazilTelephoneAnswer: 'The phone number entered is invalid',
      badBrazilCEPAnswer: 'The CEP entered is invalid',
      badBrazilCPFAnswer: 'The CPF entered is invalid',
      badPlPesel: 'The PESEL entered is invalid',
      badPlNip: 'The NIP entered is invalid',
      badPlRegon: 'The REGON entered is invalid',
      badreCaptcha: 'Please confirm that you are not a bot',
      passwordComplexityStart: 'Password must contain at least ',
      passwordComplexitySeparator: ', ',
      passwordComplexityUppercaseInfo: ' uppercase letter(s)',
      passwordComplexityLowercaseInfo: ' lowercase letter(s)',
      passwordComplexitySpecialCharsInfo: ' special character(s)',
      passwordComplexityNumericCharsInfo: ' numeric character(s)',
      passwordComplexityEnd: '.'
    }
  });

})(jQuery, window);
