/*
 * jQuery multiselect enhancer
 *
 * Copyright (c) 2009 Borgar Þorsteinsson (borgar.net)
 * Dual licensed under the MIT and GPL licenses.
 * http://docs.jquery.com/License
 *
 */
(function($){

  var _defaultConfig = {
    filter      : '<div class="filter"><label>Filter items:</label>\n<input type="text" class="filter" /></div>',
    clearfilter : '<a href="#" title="Clear filter">[X]</a>',
    setheight   : false,
    boundarysearch : true,
    setall      : '<a href="#" class="fill" title="Select visible items">Select</a>',
    setnone     : '<a href="#" class="clear" title="Deselect visible items">Clear</a>',
    item        : '<li><input/><label/></li>',
    container   : '<ul/>',
    selected    : 'selected',
    context     : 'fieldset'
  };

  var _rxcache = {};
  var _perName = {};

  function escRE (s) { return s.replace(/[.*+?^${}()|[\]\/\\]/g, '\\$0'); }

  // assign an automatic id to an element if it is missing one
  // if the element has a name attr., then use that as the basis for the id
  var _idx = new Date().getTime(), _nmx = {};
  function autoId ( elm ) {
    if (elm) {
      elm.id = elm.id || 'auto_' + ( ++_idx );
      return elm.id;
    }
    return 'auto_' + ( ++_idx );
  }

  // insert a whitespace node after an element
  function pad ( elm ) {
    return elm.parentNode.insertBefore(
      document.createTextNode(' '),
      elm.nextSibling
    );
  }

  // find a common ancestor of a JQ set
  // TODO: this should use ancestry if available, possibly compareDocumentPositions?
  function ancestor ( elms ) {
    var p = elms.eq(0).parents(),
        has = !1, i = 0,
        curr;
    while ( i < p.length && !has ) {
      // move up first element's parent chain
      curr = p.eq( i++ );
      for (var j=0; j<elms.length; j++) {
        // every element must be contained by "current"
        has = elms.eq( j ).parents().index( curr ) != -1;
        if (!has) { break; }
      }
    }
    // if one of the elements isn't in the dom, then we return an empty set
    return ( curr.length ) ? curr : $( [] );
  }

  // run through a name-set and brand is, or it's LI ancestors depending on checked state
  function selectReview ( item, cls ) {
    if (!item.name) return;
    $( '[name=' + item.name + ']', item.form ).each(function() {
      var i = $( this ), c = i.closest( 'li' );
      ( c.length ? c : i ).toggleClass( cls, !this.disabled && this.checked );
    });
  }

  // set the (checked) state of a set of elements
  function setState ( elms, attr, state, cls ) {
    elms.filter(':visible:not(:disabled)').attr( attr, state );
    cls && selectReview( elms.get(0), cls );
  }

  // add select-all button
  function addSelectAll ( ctrl, config ) {
    if ( !config.setall ) return;
    var n = $( config.setall )
      .click(function () {
        var fs = $( this ).closest( 'fieldset' );
        setState( fs.find(':checkbox,:radio'), 'checked', true, config.selected );
        fs.trigger('change');
        return false;
      })
      .appendTo( ctrl );
    pad( n[0] );
  }

  // add select-none buttons
  function addSelectNone ( ctrl, config ) {
    if ( !config.setnone ) return;
    var n = $( config.setnone )
      .click(function () {
        var fs = $( this ).closest( 'fieldset' );
        setState( fs.find(':checkbox,:radio'), 'checked', false, config.selected );
        fs.trigger('change');
        return false;
      })
      .appendTo( ctrl );
    pad( n[0] );
  }


  //
  function addFilter ( ctrl, config ) {
    if ( !config.filter ) return;
    // add search field
    var id  = autoId();
    var n   = $( config.filter );
    var clr = $( config.clearfilter ).hide()
                  .click(function(){
                    $( '#' + id ).val( '' ).trigger( 'keyup' );
                    return false;
                  });
    n.find( '*' )
      .andSelf()
        .filter( 'label' )
          .attr( 'for', id )
          .end()
        .filter( 'input' )
          .after( clr )
          .attr({ 'id':id }) // , 'name':id
          .data( 'multiselect.type', config.boundarysearch ? '(^|\\W)[\\s\\n]*' : '' )
          .keyup(function () {
            var v = $.trim( this.value ),
                r = $( this ).data( 'multiselect.type' ),
                t = _rxcache[v] || (_rxcache[v] = new RegExp( r + escRE( v ), 'i' ));
            $( this ).closest( 'fieldset' ).find( 'li' )
              .css('display','')
              .filter(function (e) {
                return !t.test( $( this ).text() );
              })
              // TODO: add highlight to these items
              .hide();
            // has value?
            clr[ v ? 'show' : 'hide' ]();
          });

      n.appendTo( ctrl );
      
      // remove the filters when form submits
      var form = ctrl.closest( 'form' ), 
          filters = form.data( 'multiselect.filters' );
      if ( filters ) {
        filters.push( n[0] );
      }
      else {
        filters = [ n[0] ];
        form
          .data( 'multiselect.filters', filters );
        $( 'html' )
          .bind( 'submit', function (e) {
            $( $( e.target ).data( 'multiselect.filters' ) ).remove();
          });
      }
  }


  $.fn.multiselect = function ( config ) {
    var cfg = $.extend( {}, _defaultConfig, config );

    // mode 1: user does this:  $(':radio[name=somename]').multiselect()
    // mode 2: user does this:  $('tag.formcontainer').multiselect()
    // mode 3: user does this:  $('#container, :radio').multiselect()

    return this.each(function(){

      var name, container, 
          item = $( this ), 
          form = this.form, 
          fpre = $.data( form ) + '_',
          cid, ctx;

      // item is a radio or checkbox
      if ( item.is( ':radio,:checkbox' ) ) {

        // ensure that the control is contained by a fieldset
        container = item.closest( 'fieldset' );
        if ( !container.length ) {
          container = ancestor( $( '[name='+name+']', form ) ).wrap( '<fieldset />' );
        }

        // only run once per fieldset "context"
        name = item.attr( 'name' );
        ctx = ( cfg.context == 'form' ) ? fpre : autoId( container[0] );
        if ( !name || _perName[ ctx + name ] ) return;

      }

      // item is a fieldset
      else if ( item.is( 'fieldset' ) ) {

        container = item;

        item = item.find( ':radio,:checkbox' ).eq(0);

        name = item.attr( 'name' );
        ctx = ( cfg.context == 'form' ) ? fpre : autoId( container[0] );
        if ( !name || _perName[ fpre + name ] ) return;
 
      }

      // TODO: add a converter for select boxes here

      // item is a "container" of some other sort
      else {

        container = item.closest( 'fieldset' );
        if ( !container.length ) {
          container = item.find( '[name='+name+']', form ).wrapAll( '<fieldset />' );
        }

        container = container.find(':radio,:checkbox');

        name = item.attr('name');
        ctx = ( cfg.context == 'form' ) ? fpre : autoId( container[0] );
        if ( !name || _perName[ fpre + name ] ) return;

      }

      // register this controlset
      // - additionally prevents processing og this control-group again
      ctx = ( cfg.context == 'form' ) ? fpre : autoId( container[0] );
      _perName[ ctx + name ] = {
        config    : cfg,
        last      : null,
        container : container
      };

      var cb = $( ':checkbox[name=' + name + '],:radio[name=' + name + ']', container );

      // add helper controls
      if ( item.is(':checkbox') ) {
        addSelectAll( container, cfg );
        
        // shift click handler
        cb
          .data( 'multiselect', ctx )
          .click(function(e){
            var c = _perName[ $(this).data( 'multiselect' ) + this.name ];
            if ( e.shiftKey && c.last ) {
              // select [last] through [this]
              var s = $( ':checkbox[name=' + name + '],:radio[name=' + name + ']', c.container ),
                  a = s.index( this ),
                  b = s.index( c.last );
              setState( s.slice( Math.min( a, b ), Math.max( a, b ) ), 'checked', true, c.config.selected );
              // prevent text selection
              var d = document, ds = d && d.selection, w = window;
              if ( w.getSelection ) {
                w.getSelection().removeAllRanges();
              }
              else if ( ds ) {
                ds.empty();
              } 
            }
            if ( !e.shiftKey || !c.last ) {
              c.last = this;
            }
          });

      }
      addSelectNone( container, cfg );
      addFilter( container, cfg );

      if ( cfg.selected ) {
        cb.click(function() { selectReview( this, cfg.selected ); });
        selectReview( cb[0], cfg.selected );
      }

    });
  };

})(jQuery);