/*

* # Semantic - Form Validation
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2014 Contributor
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/

;(function ( $, window, document, undefined ) {

$.fn.form = function(fields, parameters) {

var
  $allModules     = $(this),

  settings        = $.extend(true, {}, $.fn.form.settings, parameters),
  validation      = $.extend({}, $.fn.form.settings.defaults, fields),

  namespace       = settings.namespace,
  metadata        = settings.metadata,
  selector        = settings.selector,
  className       = settings.className,
  error           = settings.error,

  eventNamespace  = '.' + namespace,
  moduleNamespace = 'module-' + namespace,

  moduleSelector  = $allModules.selector || '',

  time            = new Date().getTime(),
  performance     = [],

  query           = arguments[0],
  methodInvoked   = (typeof query == 'string'),
  queryArguments  = [].slice.call(arguments, 1),
  returnedValue
;
$allModules
  .each(function() {
    var
      $module    = $(this),
      $field     = $(this).find(selector.field),
      $group     = $(this).find(selector.group),
      $message   = $(this).find(selector.message),
      $prompt    = $(this).find(selector.prompt),
      $submit    = $(this).find(selector.submit),

      formErrors = [],

      element    = this,
      instance   = $module.data(moduleNamespace),
      module
    ;

    module      = {

      initialize: function() {
        module.verbose('Initializing form validation', $module, validation, settings);
        module.bindEvents();
        module.instantiate();
      },

      instantiate: function() {
        module.verbose('Storing instance of module', module);
        instance = module;
        $module
          .data(moduleNamespace, module)
        ;
      },

      destroy: function() {
        module.verbose('Destroying previous module', instance);
        module.removeEvents();
        $module
          .removeData(moduleNamespace)
        ;
      },

      refresh: function() {
        module.verbose('Refreshing selector cache');
        $field = $module.find(selector.field);
      },

      submit: function() {
        module.verbose('Submitting form', $module);
        $module
          .submit()
        ;
      },

      attachEvents: function(selector, action) {
        action = action || 'submit';
        $(selector)
          .on('click', function(event) {
            module[action]();
            event.preventDefault();
          })
        ;
      },

      bindEvents: function() {

        if(settings.keyboardShortcuts) {
          $field
            .on('keydown' + eventNamespace, module.event.field.keydown)
          ;
        }
        $module
          .on('submit' + eventNamespace, module.validate.form)
        ;
        $field
          .on('blur' + eventNamespace, module.event.field.blur)
        ;
        // attach submit events
        module.attachEvents($submit, 'submit');

        $field
          .each(function() {
            var
              type       = $(this).prop('type'),
              inputEvent = module.get.changeEvent(type)
            ;
            $(this)
              .on(inputEvent + eventNamespace, module.event.field.change)
            ;
          })
        ;
      },

      removeEvents: function() {
        $module
          .off(eventNamespace)
        ;
        $field
          .off(eventNamespace)
        ;
        $submit
          .off(eventNamespace)
        ;
        $field
          .off(eventNamespace)
        ;
      },

      event: {
        field: {
          keydown: function(event) {
            var
              $field  = $(this),
              key     = event.which,
              keyCode = {
                enter  : 13,
                escape : 27
              }
            ;
            if( key == keyCode.escape) {
              module.verbose('Escape key pressed blurring field');
              $field
                .blur()
              ;
            }
            if(!event.ctrlKey && key == keyCode.enter && $field.is(selector.input) && $field.not(selector.checkbox).size() > 0 ) {
              module.debug('Enter key pressed, submitting form');
              $submit
                .addClass(className.down)
              ;
              $field
                .one('keyup' + eventNamespace, module.event.field.keyup)
              ;
            }
          },
          keyup: function() {
            module.verbose('Doing keyboard shortcut form submit');
            $submit.removeClass(className.down);
            module.submit();
          },
          blur: function() {
            var
              $field      = $(this),
              $fieldGroup = $field.closest($group)
            ;
            if( $fieldGroup.hasClass(className.error) ) {
              module.debug('Revalidating field', $field,  module.get.validation($field));
              module.validate.field( module.get.validation($field) );
            }
            else if(settings.on == 'blur' || settings.on == 'change') {
              module.validate.field( module.get.validation($field) );
            }
          },
          change: function() {
            var
              $field      = $(this),
              $fieldGroup = $field.closest($group)
            ;
            if(settings.on == 'change' || ( $fieldGroup.hasClass(className.error) && settings.revalidate) ) {
              clearTimeout(module.timer);
              module.timer = setTimeout(function() {
                module.debug('Revalidating field', $field,  module.get.validation($field));
                module.validate.field( module.get.validation($field) );
              }, settings.delay);
            }
          }
        }

      },

      get: {
        changeEvent: function(type) {
          if(type == 'checkbox' || type == 'radio' || type == 'hidden') {
            return 'change';
          }
          else {
            return (document.createElement('input').oninput !== undefined)
              ? 'input'
              : (document.createElement('input').onpropertychange !== undefined)
                ? 'propertychange'
                : 'keyup'
            ;
          }
        },
        field: function(identifier) {
          module.verbose('Finding field with identifier', identifier);
          if( $field.filter('#' + identifier).size() > 0 ) {
            return $field.filter('#' + identifier);
          }
          else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
            return $field.filter('[name="' + identifier +'"]');
          }
          else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
            return $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]');
          }
          return $('<input/>');
        },
        validation: function($field) {
          var
            rules
          ;
          $.each(validation, function(fieldName, field) {
            if( module.get.field(field.identifier).get(0) == $field.get(0) ) {
              rules = field;
            }
          });
          return rules || false;
        }
      },

      has: {

        field: function(identifier) {
          module.verbose('Checking for existence of a field with identifier', identifier);
          if( $field.filter('#' + identifier).size() > 0 ) {
            return true;
          }
          else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
            return true;
          }
          else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
            return true;
          }
          return false;
        }

      },

      add: {
        prompt: function(identifier, errors) {
          var
            $field       = module.get.field(identifier),
            $fieldGroup  = $field.closest($group),
            $prompt      = $fieldGroup.find(selector.prompt),
            promptExists = ($prompt.size() !== 0)
          ;
          errors = (typeof errors == 'string')
            ? [errors]
            : errors
          ;
          module.verbose('Adding field error state', identifier);
          $fieldGroup
            .addClass(className.error)
          ;
          if(settings.inline) {
            if(!promptExists) {
              $prompt = settings.templates.prompt(errors);
              $prompt
                .appendTo($fieldGroup)
              ;
            }
            $prompt
              .html(errors[0])
            ;
            if(!promptExists) {
              if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
                module.verbose('Displaying error with css transition', settings.transition);
                $prompt.transition(settings.transition + ' in', settings.duration);
              }
              else {
                module.verbose('Displaying error with fallback javascript animation');
                $prompt
                  .fadeIn(settings.duration)
                ;
              }
            }
            else {
              module.verbose('Inline errors are disabled, no inline error added', identifier);
            }
          }
        },
        errors: function(errors) {
          module.debug('Adding form error messages', errors);
          $message
            .html( settings.templates.error(errors) )
          ;
        }
      },

      remove: {
        prompt: function(field) {
          var
            $field      = module.get.field(field.identifier),
            $fieldGroup = $field.closest($group),
            $prompt     = $fieldGroup.find(selector.prompt)
          ;
          $fieldGroup
            .removeClass(className.error)
          ;
          if(settings.inline && $prompt.is(':visible')) {
            module.verbose('Removing prompt for field', field);
            if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
              $prompt.transition(settings.transition + ' out', settings.duration, function() {
                $prompt.remove();
              });
            }
            else {
              $prompt
                .fadeOut(settings.duration, function(){
                  $prompt.remove();
                })
              ;
            }
          }
        }
      },

      set: {
        success: function() {
          $module
            .removeClass(className.error)
            .addClass(className.success)
          ;
        },
        error: function() {
          $module
            .removeClass(className.success)
            .addClass(className.error)
          ;
        }
      },

      validate: {

        form: function(event) {
          var
            allValid = true,
            apiRequest
          ;
          // reset errors
          formErrors = [];
          $.each(validation, function(fieldName, field) {
            if( !( module.validate.field(field) ) ) {
              allValid = false;
            }
          });
          if(allValid) {
            module.debug('Form has no validation errors, submitting');
            module.set.success();
            return $.proxy(settings.onSuccess, element)(event);
          }
          else {
            module.debug('Form has errors');
            module.set.error();
            if(!settings.inline) {
              module.add.errors(formErrors);
            }
            // prevent ajax submit
            if($module.data('moduleApi') !== undefined) {
              event.stopImmediatePropagation();
            }
            return $.proxy(settings.onFailure, element)(formErrors);
          }
        },

        // takes a validation object and returns whether field passes validation
        field: function(field) {
          var
            $field      = module.get.field(field.identifier),
            fieldValid  = true,
            fieldErrors = []
          ;
          if($field.prop('disabled')) {
            module.debug('Field is disabled. Skipping', field.identifier);
            fieldValid = true;
          }
          else if(field.optional && $.trim($field.val()) === ''){
            module.debug('Field is optional and empty. Skipping', field.identifier);
            fieldValid = true;
          }
          else if(field.rules !== undefined) {
            $.each(field.rules, function(index, rule) {
              if( module.has.field(field.identifier) && !( module.validate.rule(field, rule) ) ) {
                module.debug('Field is invalid', field.identifier, rule.type);
                fieldErrors.push(rule.prompt);
                fieldValid = false;
              }
            });
          }
          if(fieldValid) {
            module.remove.prompt(field, fieldErrors);
            $.proxy(settings.onValid, $field)();
          }
          else {
            formErrors = formErrors.concat(fieldErrors);
            module.add.prompt(field.identifier, fieldErrors);
            $.proxy(settings.onInvalid, $field)(fieldErrors);
            return false;
          }
          return true;
        },

        // takes validation rule and returns whether field passes rule
        rule: function(field, validation) {
          var
            $field        = module.get.field(field.identifier),
            type          = validation.type,
            value         = $.trim($field.val() + ''),

            bracketRegExp = /\[(.*)\]/i,
            bracket       = bracketRegExp.exec(type),
            isValid       = true,
            ancillary,
            functionType
          ;
          // if bracket notation is used, pass in extra parameters
          if(bracket !== undefined && bracket !== null) {
            ancillary    = '' + bracket[1];
            functionType = type.replace(bracket[0], '');
            isValid      = $.proxy(settings.rules[functionType], element)(value, ancillary);
          }
          // normal notation
          else {
            isValid = $.proxy(settings.rules[type], $field)(value);
          }
          return isValid;
        }
      },

      setting: function(name, value) {
        if( $.isPlainObject(name) ) {
          $.extend(true, settings, name);
        }
        else if(value !== undefined) {
          settings[name] = value;
        }
        else {
          return settings[name];
        }
      },
      internal: function(name, value) {
        if( $.isPlainObject(name) ) {
          $.extend(true, module, name);
        }
        else if(value !== undefined) {
          module[name] = value;
        }
        else {
          return module[name];
        }
      },
      debug: function() {
        if(settings.debug) {
          if(settings.performance) {
            module.performance.log(arguments);
          }
          else {
            module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
            module.debug.apply(console, arguments);
          }
        }
      },
      verbose: function() {
        if(settings.verbose && settings.debug) {
          if(settings.performance) {
            module.performance.log(arguments);
          }
          else {
            module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
            module.verbose.apply(console, arguments);
          }
        }
      },
      error: function() {
        module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
        module.error.apply(console, arguments);
      },
      performance: {
        log: function(message) {
          var
            currentTime,
            executionTime,
            previousTime
          ;
          if(settings.performance) {
            currentTime   = new Date().getTime();
            previousTime  = time || currentTime;
            executionTime = currentTime - previousTime;
            time          = currentTime;
            performance.push({
              'Name'           : message[0],
              'Arguments'      : [].slice.call(message, 1) || '',
              'Element'        : element,
              'Execution Time' : executionTime
            });
          }
          clearTimeout(module.performance.timer);
          module.performance.timer = setTimeout(module.performance.display, 100);
        },
        display: function() {
          var
            title = settings.name + ':',
            totalTime = 0
          ;
          time = false;
          clearTimeout(module.performance.timer);
          $.each(performance, function(index, data) {
            totalTime += data['Execution Time'];
          });
          title += ' ' + totalTime + 'ms';
          if(moduleSelector) {
            title += ' \'' + moduleSelector + '\'';
          }
          if($allModules.size() > 1) {
            title += ' ' + '(' + $allModules.size() + ')';
          }
          if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
            console.groupCollapsed(title);
            if(console.table) {
              console.table(performance);
            }
            else {
              $.each(performance, function(index, data) {
                console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
              });
            }
            console.groupEnd();
          }
          performance = [];
        }
      },
      invoke: function(query, passedArguments, context) {
        var
          object = instance,
          maxDepth,
          found,
          response
        ;
        passedArguments = passedArguments || queryArguments;
        context         = element         || context;
        if(typeof query == 'string' && object !== undefined) {
          query    = query.split(/[\. ]/);
          maxDepth = query.length - 1;
          $.each(query, function(depth, value) {
            var camelCaseValue = (depth != maxDepth)
              ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
              : query
            ;
            if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
              object = object[camelCaseValue];
            }
            else if( object[camelCaseValue] !== undefined ) {
              found = object[camelCaseValue];
              return false;
            }
            else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
              object = object[value];
            }
            else if( object[value] !== undefined ) {
              found = object[value];
              return false;
            }
            else {
              return false;
            }
          });
        }
        if ( $.isFunction( found ) ) {
          response = found.apply(context, passedArguments);
        }
        else if(found !== undefined) {
          response = found;
        }
        if($.isArray(returnedValue)) {
          returnedValue.push(response);
        }
        else if(returnedValue !== undefined) {
          returnedValue = [returnedValue, response];
        }
        else if(response !== undefined) {
          returnedValue = response;
        }
        return found;
      }
    };
    if(methodInvoked) {
      if(instance === undefined) {
        module.initialize();
      }
      module.invoke(query);
    }
    else {
      if(instance !== undefined) {
        module.destroy();
      }
      module.initialize();
    }

  })
;

return (returnedValue !== undefined)
  ? returnedValue
  : this
;

};

$.fn.form.settings = {

name              : 'Form',
namespace         : 'form',

debug             : false,
verbose           : true,
performance       : true,

keyboardShortcuts : true,
on                : 'submit',
inline            : false,

delay             : 200,
revalidate        : true,

transition        : 'scale',
duration          : 200,

onValid           : function() {},
onInvalid         : function() {},
onSuccess         : function() { return true; },
onFailure         : function() { return false; },

metadata : {
  validate: 'validate'
},

selector : {
  message : '.error.message',
  field   : 'input, textarea, select',
  group   : '.field',
  checkbox: 'input[type="checkbox"], input[type="radio"]',
  input   : 'input',
  prompt  : '.prompt',
  submit  : '.submit'
},

className : {
  error   : 'error',
  success : 'success',
  down    : 'down',
  label   : 'ui prompt label'
},

error: {
  method   : 'The method you called is not defined.'
},

templates: {
  error: function(errors) {
    var
      html = '<ul class="list">'
    ;
    $.each(errors, function(index, value) {
      html += '<li>' + value + '</li>';
    });
    html += '</ul>';
    return $(html);
  },
  prompt: function(errors) {
    return $('<div/>')
      .addClass('ui red pointing prompt label')
      .html(errors[0])
    ;
  }
},

rules: {

  // checkbox checked
  checked: function() {
    return ($(this).filter(':checked').size() > 0);
  },

  // value contains (text)
  contains: function(value, text) {
    text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    return (value.search(text) !== -1);
  },

  // is most likely an email
  email: function(value){
    var
      emailRegExp = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", "i")
    ;
    return emailRegExp.test(value);
  },

  // is not empty or blank string
  empty: function(value) {
    return !(value === undefined || '' === value);
  },

  // is valid integer
  integer: function(value, range) {
    var
      intRegExp = /^\-?\d+$/,
      min,
      max,
      parts
    ;
    if (range === undefined || range === '' || range === '..') {
      // do nothing
    }
    else if (range.indexOf('..') == -1) {
      if (intRegExp.test(range)) {
        min = max = range - 0;
      }
    }
    else {
      parts = range.split('..', 2);
      if (intRegExp.test(parts[0])) {
        min = parts[0] - 0;
      }
      if (intRegExp.test(parts[1])) {
        max = parts[1] - 0;
      }
    }
    return (
      intRegExp.test(value) &&
      (min === undefined || value >= min) &&
      (max === undefined || value <= max)
    );
  },

  // is exactly value
  is: function(value, text) {
    return (value == text);
  },

  // is at least string length
  length: function(value, requiredLength) {
    return (value !== undefined)
      ? (value.length >= requiredLength)
      : false
    ;
  },

  // matches another field
  match: function(value, fieldIdentifier) {
    // use either id or name of field
    var
      $form = $(this),
      matchingValue
    ;
    if($form.find('#' + fieldIdentifier).size() > 0) {
      matchingValue = $form.find('#' + fieldIdentifier).val();
    }
    else if($form.find('[name="' + fieldIdentifier +'"]').size() > 0) {
      matchingValue = $form.find('[name="' + fieldIdentifier + '"]').val();
    }
    else if( $form.find('[data-validate="'+ fieldIdentifier +'"]').size() > 0 ) {
      matchingValue = $form.find('[data-validate="'+ fieldIdentifier +'"]').val();
    }
    return (matchingValue !== undefined)
      ? ( value.toString() == matchingValue.toString() )
      : false
    ;
  },

  // string length is less than max length
  maxLength: function(value, maxLength) {
    return (value !== undefined)
      ? (value.length <= maxLength)
      : false
    ;
  },

  // value is not exactly notValue
  not: function(value, notValue) {
    return (value != notValue);
  },

  // value is most likely url
  url: function(value) {
    var
      urlRegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
    ;
    return urlRegExp.test(value);
  }
}

};

})( jQuery, window , document );