/*
 *  jQuery multiselect plugin
 *  
 *  Version 1.0
 *  URL: http://borgar.net/svn/public/trunk/javascript/jquery.myplugin/
 *
 *  Licenced under dual MIT/GPL license.
 *    GPL: http://www.opensource.org/licenses/gpl-2.0.php
 *    MIT: http://www.opensource.org/licenses/mit-license.php
 *  
*//*global
  jQuery
*/
(function($){

  // todo: there is a problem in that selections are not stored across refreshes, user may expect this

  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,
    setall      : '<a href="#" class="fill" title="Select visible items">Select</a>',
    setnone     : '<a href="#" class="clear" title="Deselect visible items">Clear</a>',
    toggler     : '<input type="checkbox">',
    item        : '<li><input/><label/></li>',
    container   : '<ul/>',
    selected    : 'selected',
    activeclass : 'multiselect',
    selectmode  : 'set'     // "links"/"set" = (setall, setnone),    ""/falsy = (no selection controls),  "toggle" = toggler
  };

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

  function encodeRE ( s ) { return s.replace(/[.*+?^${}()|[\]\/\\]/g, '\\$0'); }
  function getContext ( n ) { return ( n && n.form !== null ) ? n.form : n.ownerDocument.body || document.body; }

  // 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 ) {
      return elm.id = elm.id || autoId();
    }
    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 it, or it's LI ancestors depending on checked state
  function selectReview ( item, cls ) {
    if (!item.name) return;
    $( '[name=' + item.name + ']', getContext( item ) ).each(function() {
      var i = $( this ), c = i.closest( 'li' );
      if ( c.length ) {
        // knock out selectability within the box (experimental):
        c.attr( 'unselectable', 'on' );
        c.css( '-moz-user-select', 'none' );
        i = c;
      }
      i.toggleClass( cls, !this.disabled && this.checked );
    });
    document.body.className += '';
  }

  // 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.selectmode || !/^(set|links)$/.test(config.selectmode) || !config.setall ) return;
    var n = $( config.setall )
      .click(function () {
        var fs = $( this ).closest( 'fieldset' );
        setState( fs.find( ':checkbox,:radio' ), 'checked', true, config.selected );
        return false;
      })
      .appendTo( ctrl );
    pad( n[0] );
  }


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


  // add select-none buttons
  function addSelectToggler ( ctrl, config ) {
    if ( !config.selectmode || !/^(toggler?)$/.test(config.selectmode) || !config.toggler ) return;

    ctrl.bind( 'change', function( e ){
      var elm = $( e.target );
      var ctrl = $( this );
      if ( elm.is( ':checkbox,:radio' ) && !elm.hasClass( 'toggler' ) ) {

        var chk = ctrl.find( ':checkbox,:radio' ).filter(':not(.toggler)');
        var sel = chk.filter(':checked');
        var toggler = ctrl.data( 'toggler' );

        toggler.checked = sel.length;
        toggler.indeterminate = sel.length !== 0 && sel.length !== chk.length;

      }
    });

    var n = $( config.toggler )
      .addClass( 'toggler' )
      .bind('click', function () { // needs to be click so .indeterminate works right
        var chk = $( this );
        var fs = chk.closest( 'fieldset' );
        var chks = fs.find( ':checkbox,:radio' ).filter(function(){ return chk[0] !== this; });
        setState( chks, 'checked', this.checked, config.selected );
        // webkit does not like this event to be canceled
      })
      .appendTo( ctrl );
    
    ctrl.data( 'toggler', n[0] );
    ctrl.find( ':checkbox,:radio' ).eq(0).trigger('change');
    
    pad( n[0] );
  }
  


  //
  function addFilter ( ctrl, config ) {
    if ( !config.filter ) return;
    // add search field
    // ... onsubmit should remove this 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 })
          .keyup(function () {
            var v = $.trim( this.value );
            var t = _rxcache[v] || (_rxcache[v] = new RegExp( '(^|\\W)[\\s\\n]*' + encodeRE( v ), 'i' ));
            $( this ).closest( 'fieldset' ).find( 'li' )
              .show()
              .filter(function (e) {
                return !t.test( $( this ).text() );
              })
              // TODO: add highlight to these items
              .hide();
            // has value?
            clr[ v ? 'show' : 'hide' ]();
          });

      n.appendTo( ctrl );
  }


  $.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 = getContext( this ),  // controls are scoped by their forms, or body element if not in a form
          fpre = autoId( form ) + '_';

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

        name = item.attr('name');
        if ( !name || _perName[ fpre + name ] ) return;

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

      }

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

        container = item;

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

        name = item.attr( 'name' );
        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');
        if ( !name || _perName[ fpre + name ] ) return;

      }

      // register this controlset
      // - additionally prevents processing og this control-group again
      _perName[ fpre + 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 );
        addSelectToggler( container, cfg ); 

        // shift click handler
        container.bind('mouseup',function(e){
          // resolve input or label from target
          
          var t = $( e.target ).closest(':input,label', container);
          if ( !t.length ) return;
          if ( t.is('label') ) {
            var id = t.attr('for');
            t = id ? $( '#' + id ) : t.find(':checkbox,:radio').eq(0);
          }

          var name = t[0].name;
          var c = _perName[ getContext( t[0] ).id + '_' + name ];

          if ( c && e.shiftKey && c.last ) { // TODO: store radio/checkbox type in c
            // select [last] through [this]
            var s = $( ':checkbox[name=' + name + '],:radio[name=' + name + ']', c.container ),
                a = s.index( t[0] ),
                b = s.index( c.last );
            setState( s.slice( Math.min( a, b ), ((Math.max( a, b )==a) ? a+1 : 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 ( c && (!e.shiftKey || !c.last) ) {
            c.last = t[0];
          }
          
        });

      }
      addSelectNone( container, cfg );
      addFilter( container, cfg );
      container.addClass( cfg.activeclass );
      
      if ( cfg.selected ) {
        cb.click(function() { selectReview( this, cfg.selected ); });
        selectReview( cb[0], cfg.selected );
      }

    });
  };
  
  // window._perName = _perName;

})(jQuery);
