(function($){

  // default settings
  $.validate = {
    config: {
      alert : true
    }
  };


  // add the closest function if it doesn't exist
  if (!$.fn.closest) {
    $.fn.closest = function ( s ) {
      return $.unique(
        this.map(function(a,n){
          var elm = $( n );
          return elm.is(s) ? n : (elm.parents( s )[ 0 ] || null);
        })
      );
    };
  }
  
  
  function normalize ( ctx ) {
    var inp = ctx.map(function ( a,n ) {
      var elm = $( n );
      return ( elm.is( ':input' ) ) ? n : elm.find( ':input' ).get();
    })
    return $( $.unique( inp ) );
  }


  // main validation function
  function validate ( context, conf ) {

    var form = $( context );
    var valid = true;

    var inputs = normalize( context );

    inputs = inputs.filter( ':input[name].required, .required :input[name]' );
    inputs.closest( '.required' ).removeClass( 'formerror-missing' );

    // pass 1 - serialize
    var serial = inputs.getValues();

    // pass 2 - are there any empty arrays in serial?
    var missing = [];
    for ( var name in serial ) {
      if ( serial[ name ].length === 0 ) {
        // this form is invalid
        valid = false;
        var ips = inputs.filter( '[name=' + name + ']' );
        var ctrl = ips.closest( '.required' ).addClass( 'formerror-missing' );

        // create a human readable error message from 
        // TODO: allow form elemets to set a custom message
        var err = '', head = '';
        if ( ips.length == 1 ) {
          if ( ips.attr( 'id' ) )
            err = form.find( 'label[for=' + this.id + ']' ).text();
          if ( !err )
            err = ctrl.find( 'label' ).text();
          if ( !err )
            err = ips.attr( 'title' ) || ctrl.attr( 'title' );
          err = err.replace(/^[\s\*]*|^[\s\*]*/, '') || 'Ótitlaður reitur';  // TODO: config
          head = ips.eq(0).closest( 'fieldset' ).find( ':heading:first' )
                    .text().replace(/^[\s\*]*|^[\s\*]*/, '');
          if ( head )
            head = '[' + head + ']';
        }
        else {
          err = '';
          var fs = $.unique( ips.closest( 'fieldset' ) );
          var hd = $( fs ).find( ':header' ).map(function () {
            return $( this ).text().replace(/^[\s\*]*|^[\s\*]*/, '');
          });
          err = hd.get().join( ' / ' );
        }
        missing.push( err + ' ' + head  );
      }
    }

    // report errors
    if ( !valid ) {

      if ( conf.alert ) {
        alert( 'Eftirfarandi gildi þarf að fylla út:\n' + 
          '\n* ' + missing.join( '\n* ' ) 
        );
      }
      // todo: add proper error texts to nodes
      ips.eq(0).focus();
    }

    return valid;
  }

  $.fn.validate = function ( conf ) {
    var valid = true;
    // fixme: only validata forms/inputs?
    this.each(function () {
      // todo: supress alerts if forms.length > 1 ?
      $( this ).validate( 
          $.extend( {}, $(this).data('validate'), conf ) 
      );
      valid = validate( this, conf ) && valid;
    });
    return valid;
  }


  $.fn.values = function ( obj ) {
    
    var serial = obj || {};
    var inputs = normalize( this );

    // setter
    if ( obj ) {
      for ( var key in serial ) {
        
        var val = serial[ key ];
        var inp = inputs.filter( '[name=' + key + ']' );
        if ( inp.is( ':radio' ) ) {
          // find the checkbox that has this value
          inp.filter( '[value=' + val + ']' ).attr('checked', true);
        }
        else if ( inp.is( ':checkbox' ) ) {
          // TODO: this needs a more defined action, does name=false or [name=name][val!=val] set unchecked? ...
          // find the checkbox that has this value
          inp.filter( '[value=' + key + ']' ).attr('checked', true);
        }
        else {
          inp.val( val );
        }
      }
      
    }

    // getter
    else {
      
      inputs.each(function(){
        var inp = $( this ),
            val = inp.val(),
            data = serial[ this.name ] || [];
        if ( inp.is( ':checkbox, :radio' ) ) {
          if ( inp.attr( 'checked' ) ) {
            data.push( val );
          }
        }
        else if ( /\S/.test( val ) ) {
          data.push( val );
        }
        serial[ this.name ] = data;
      });

    }
    return serial;

  };
  

  $.fn.autovalidate = function ( conf ) {
    this.filter('form').each(function () {
      
      $( this )
        .data('validate', $.extend( {}, $.validate.config, conf ) )
        .bind( 'submit', function ( e ) {
          $( this ).validate( conf );
        });

    });
    return this;
  }
  
})(jQuery);
