(function($) {

jQuery.fn.borderWidth = function() { return $(this).outerWidth() - $(this).innerWidth(); };
jQuery.fn.marginWidth = function() { return $(this).outerWidth(true) - $(this).outerWidth(); };
jQuery.fn.paddingWidth = function() { return $(this).innerWidth() - $(this).width(); };
jQuery.fn.extraWidth = function() { return $(this).outerWidth(true) - $(this).width(); };
jQuery.fn.offsetFrom = function($e) { 
  return {
    left: $(this).offset().left - $e.offset().left,
    top: $(this).offset().top - $e.offset().top
  };
};

jQuery.fn.maxWidth = function() {
  var max = 0;
  $(this).each(function() {
    if($(this).width() > max) max = $(this).width();
  });
  return max;
}

jQuery.fn.sb = function(o) {
  if($.browser.msie && $.browser.version < 7) return $(this);
  
  o = $.extend({
    acTimeout: 800,               // time between each keyup for the user to create a search string
    animDuration: 300,            // time to open/close dropdown in ms
    ddCtx: 'body',                // body | self | any selector | a function that returns a selector (the original select is the context)
    dropupThreshold: 150,         // the minimum amount of extra space required above the selectbox for it to display a dropup
    fixedWidth: false,            // if false, dropdown expands to widest and display conforms to whatever is selected
    maxHeight: false,             // if an integer, show scrollbars if the dropdown is too tall
    maxWidth: false,              // if an integer, prevent the display/dropdown from growing past this width; longer items will be clipped
    selectboxClass: 'selectbox',  // class to apply our markup
    useTie: false,                // if jquery.tie is included and this is true, the selectbox will update dynamically
    
    // markup appended to the display, typically for styling an arrow
    arrowMarkup: "<span class='arrow_btn'><span class='interior'><span class='arrow'></span></span></span>",
    
    // given the selected element of the form <span class='text'>...</span> modify to fit the display as necessary
    displayFormat: function() {
      var label = $(this).attr("label");
      if($.trim(label) != "") return label;
      return $(this).text();
    },
    
    // formatting for the display; note that it will be wrapped with <a href='#'><span class='text'></span></a>
    optionFormat: function(ogIndex, optIndex) {
      var label = $(this).attr("label");
      if($.trim(label) != "") return label;
      return $(this).text();
    },
    
    // the function to produce optgroup markup
    optgroupFormat: function(ogIndex) {
      return "<span class='label'>" + $(this).attr("label") + "</span>";
    }
  }, o);
  
  $(this).each(function() {
    var $orig = $(this);
    var $sb = null;
    var $display = null;
    var $dd = null;
    var $items = null;
    
    if($orig.hasClass("has_sb")) { return; }
    else { $orig.addClass("has_sb"); }
    
    function loadSB() {
      // create the new markup from the old
      var optionMarkup = $orig.children().size() > 0 ? o.displayFormat.call($orig.find("option:selected")[0], 0, 0) : "&nbsp;";
      $sb = $("<div class='sb " + o.selectboxClass + " " + $orig.attr("class") + "'></div>");
      $("body").append($sb);
      $display = $("<a href='#' class='display " + $orig.attr("class") + "'><span class='value'>" + $orig.val() + "</span> <span class='text'>" + optionMarkup  + "</span>" + o.arrowMarkup + "</a>");
      $sb.append($display);
      $dd = $("<ul class='" + o.selectboxClass + " items " + $orig.attr("class") + "'></ul>");
      $sb.append($dd);
      if($orig.children().size() == 0) {
        var $emptyLi = $("<li class='selected empty'><a href='#'><span class='value'></span><span class='text'>&nbsp;</span></a></li>");
        $emptyLi.data("val", "");
        $dd.append($emptyLi);
      }
      else {
        $orig.children().each(function(i) {
          if($(this).is("optgroup")) {
            var $og = $(this);
            var $ogItem = $("<li class='optgroup'>" + o.optgroupFormat.call($og[0], i+1) + "</li>");
            var $ogList = $("<ul class='items'></ul>");
            $ogItem.append($ogList);
            $dd.append($ogItem);
            $og.children("option").each(function(j) {
              var $li = $("<li class='" + ($(this).attr("selected") ? "selected" : "" ) + " " + ($(this).attr("disabled") ? "disabled" : "" ) + "'><a href='#'><span class='value'>" + $(this).attr("value") + "</span><span class='text'>" + o.optionFormat.call(this, 0, i+1) + "</span></a></li>");
              $li.data("val", $(this).attr("value"));
              $ogList.append($li);
            });
          }
          else {
            var $li = $("<li class='" + ($(this).attr("selected") ? "selected" : "" ) + " " + ($(this).attr("disabled") ? "disabled" : "" ) + "'><a href='#'><span class='value'>" + $(this).attr("value") + "</span><span class='text'>" + o.optionFormat.call(this, 0, i+1) + "</span></a></li>")
            $li.data("val", $(this).attr("value"));
            $dd.append($li);
          }
        });
      }
      $items = $dd.find("li").not(".optgroup");
      $dd.children(":first").addClass("first");
      $dd.children(":last").addClass("last");
      $orig.hide();
    
      if(!o.fixedWidth) {
        // match display size to largest element
        var largestWidth = $sb.find(".text, .optgroup").maxWidth() + $display.extraWidth() + 1;
        $sb.width(o.maxWidth ? Math.min(o.maxWidth, largestWidth) : largestWidth);
        if($.browser.msie && $.browser.version <= 7) {
          $items.find("a").each(function() {
            $(this).css("width", "100%").width($(this).width() - $(this).paddingWidth() - $(this).borderWidth());
          });
        }
      }
      else if(o.maxWidth && $sb.width() > o.maxWidth) {
        $sb.width(o.maxWidth);
      }
      $orig.before($sb);
      
      // initialize dd and bindings
      $dd.hide();
      if(!$orig.is(":disabled")) {
        $display.click(clickSB).focus(focusSB).blur(blurSB).hover(addHoverState, removeHoverState);
        $items.not(".disabled").find("a").click(clickSBItem);
        $items.filter(".disabled").find("a").click(function() { return false; });
        $items.not(".disabled").hover(addHoverState, removeHoverState);
        $dd.find(".optgroup").hover(addHoverState, removeHoverState).click(function() { return false; });
      }
      else {
        $sb.addClass("disabled");
        $display.click(function(e) { e.preventDefault(); });
      }
      $sb.bind("close", closeSB);
      $sb.bind("destroy", destroySB);
      $orig.bind("reload", reloadSB);
      if(jQuery.fn.tie && o.useTie) {
        $orig.bind("domupdate", delayReloadSB);
      }
      $orig.focus(focusOrig);
    }
    
    function focusOrig() { $display.focus(); return false; }
    
    var delayReloadTimeout = null;
    function delayReloadSB() {
      clearTimeout(delayReloadTimeout);
      delayReloadTimeout = setTimeout(reloadSB, 30);
    }
    
    function reloadSB() {
      var isOpen = $sb.is(".open");
      var isFocused = $display.is(".focused");
      instantCloseSB();
      destroySB();
      loadSB();
      if(isOpen) {
        $display.focus();
        instantOpenSB();
      }
      else if(isFocused) {
        $display.focus();
      }
    }
    
    // unbind and remove
    function destroySB() {
      $sb.unbind().find("*").unbind();
      $sb.remove();
      $orig.unbind("reload", reloadSB).unbind("domupdate", delayReloadSB).unbind("focus", focusOrig).removeClass("has_sb").show();
    }
    
    // when the user clicks outside the sb
    function killAndUnbind() {
      killAll();
      $(document).unbind("click", killAndUnbind);
    }
    
    // trigger all sbs to close
    function killAll() {
      $(".sb." + o.selectboxClass).each(function() {
        $(this).triggerHandler("close");
      });
    }
    
    // to prevent multiple selects open at once
    function killAllButMe() {
      $(".sb." + o.selectboxClass).not($sb[0]).each(function() {
        $(this).triggerHandler("close");
      });
    }
    
    // hide and reset dropdown markup
    function closeSB() {
      if($sb.is(".open")) {
        $items.removeClass("hover");
        $(document).unbind("keyup", keyupSB);
        $(document).unbind("keydown", stopPageHotkeys);
        $(document).unbind("keydown", keydownSB);
        $dd.fadeOut(o.animDuration, function() {
          $sb.removeClass("open");
          $sb.append($dd);
        });
      }
    }
    
    function instantCloseSB() {
      $items.removeClass("hover");
      $(document).unbind("keyup", keyupSB);
      $(document).unbind("keydown", stopPageHotkeys);
      $dd.hide();
      $sb.removeClass("open");
      $sb.append($dd);
    }
    
    function getDDCtx() {
      var $ddCtx = null;
      if(o.ddCtx == "self") {
        $ddCtx = $sb;
      }
      else if($.isFunction(o.ddCtx)) {
        $ddCtx = $(o.ddCtx.call($orig[0]));
      }
      else {
        $ddCtx = $(o.ddCtx);
      }
      return $ddCtx;
    }
    
    function centerOnSelected() {
      $dd.scrollTop($dd.scrollTop() + $items.filter(".selected").offsetFrom($dd).top - $dd.height() / 2 + $items.filter(".selected").outerHeight(true) / 2);
    }
    
    // show, reposition, and reset dropdown markup
    function openSB() {
      var $ddCtx = getDDCtx();
      killAll();
      $sb.addClass("open");
      var dir = positionSB();
      $ddCtx.append($dd);
      if($.browser.msie && $.browser.version < 8) {
        // fix ie7 display bug
        $("." + o.selectboxClass + " .display").hide().show();
      }
      if(dir == "up") $dd.fadeIn(o.animDuration, centerOnSelected);
      else if(dir == "down") $dd.slideDown(o.animDuration, centerOnSelected);
      else $dd.fadeIn(o.animDuration, centerOnSelected);
      $(document).click(killAndUnbind);
      $display.focus();
    }
    
    function instantOpenSB() {
      var $ddCtx = getDDCtx();
      killAll();
      $sb.addClass("open");
      var dir = positionSB();
      $ddCtx.append($dd);
      if($.browser.msie && $.browser.version < 8) {
        // fix ie7 display bug
        $("." + o.selectboxClass + " .display").hide().show();
      }
      $dd.show();
      centerOnSelected();
      $(document).click(killAndUnbind);
      $display.focus();
    }
    
    // position dropdown based on collision detection
    function positionSB() {
      var $ddCtx = getDDCtx();
      var ddMaxHeight = 0;
      var ddX = $display.offsetFrom($ddCtx).left;
      var ddY = 0;
      var dir = "";
      
      // modify dropdown css for getting values
      $dd.removeClass("above");
      $dd.css({
        display: "block",
        maxHeight: "none",
        position: "relative",
        visibility: "hidden" 
      });
      if(!o.fixedWidth) { $dd.width($display.outerWidth() - $dd.extraWidth() + 1); }
      
      // figure out if we should show above/below the display box
      var bottomSpace = $(window).scrollTop() + $(window).height() - $display.offset().top - $display.outerHeight();
      var topSpace = $display.offset().top - $(window).scrollTop();
      var bottomOffset = $display.offsetFrom($ddCtx).top + $display.outerHeight();
      var spaceDiff = bottomSpace - topSpace + o.dropupThreshold;
      if($dd.outerHeight() < bottomSpace) {
        ddMaxHeight = o.maxHeight ? o.maxHeight : bottomSpace;
        ddY = bottomOffset;
        dir = "down";
      }
      else if($dd.outerHeight() < topSpace) {
        ddMaxHeight = o.maxHeight ? o.maxHeight : topSpace;
        ddY = $display.offsetFrom($ddCtx).top - Math.min(ddMaxHeight, $dd.outerHeight());
        dir = "up";
      }
      else if(spaceDiff >= 0) {
        ddMaxHeight = o.maxHeight ? o.maxHeight : bottomSpace;
        ddY = bottomOffset;
        dir = "down";
      }
      else if(spaceDiff < 0) {
        ddMaxHeight = o.maxHeight ? o.maxHeight : topSpace;
        ddY = $display.offsetFrom($ddCtx).top - Math.min(ddMaxHeight, $dd.outerHeight());
        dir = "up";
      }
      else {
        ddMaxHeight = o.maxHeight ? o.maxHeight : "none";
        ddY = bottomOffset;
        dir = "down";
      }
      
      // modify dropdown css for display
      var bodyX = $().jquery < "1.4.2" ? $("body").offset().left : parseInt($("body").css("margin-left"));
      var bodyY = $().jquery < "1.4.2" ? $("body").offset().top : parseInt($("body").css("margin-top"));
      $dd.css({
        display: "none",
        left: ddX + ($ddCtx[0].tagName.toLowerCase() == "body" ? bodyX : 0),
        maxHeight: ddMaxHeight,
        position: "absolute",
        top: ddY + ($ddCtx[0].tagName.toLowerCase() == "body" ? bodyY : 0),
        visibility: "visible"
      });
      if(dir == "up") $dd.addClass("above");
      return dir;
    }
    
    // when the user explicitly clicks the display
    function clickSB(e) {
      var $sb = $(this).closest("." + o.selectboxClass);
      if($sb.is(".open")) {
        closeSB();
      }
      else {
        $display.focus();
        openSB();
      }
      return false;
    }
    
    // when the user selects an item in any manner
    function selectItem() {
      var $item = $(this);
      $display.find(".value").html($item.find(".value").html());
      $display.find(".text").attr("title", $item.find(".text").html());
      $dd.find("li").removeClass("selected");
      $item.closest("li").addClass("selected");
      var oldVal = $orig.val();
      var newVal = $item.closest("li").data("val");
      $orig.val(newVal);
      $display.find(".text").html(o.displayFormat.call($orig.find("option:selected")[0]));
      if(oldVal != newVal) {
        $orig.change();
      }
    }
    
    // when the user explicitly clicks an item
    function clickSBItem(e) {
      selectItem.call(this);
      killAndUnbind();
      $display.focus();
      return false;
    }
    
    // helper functions for matching on keyup
    var searchTerm = "";
    var cstTimeout = null;
    function clearSearchTerm() {
      searchTerm = "";
    }
    function findMatchingItem(term) {
      var ts = "";
      var $available = $items.not(".disabled");
      for(var i=0; i < $available.size(); i++) {
        var t = $available.eq(i).find(".text").text();
        ts += t + " ";
        if(t.toLowerCase().match("^" + term.toLowerCase()) == term.toLowerCase()) {
          return $available.eq(i);
        }
      }
      return null;
    }
    function selectMatchingItem(text) {
      var $matchingItem = findMatchingItem(text);
      if($matchingItem != null) {
        selectItem.call($matchingItem[0]);
        return true;
      }
      return false;
    }
    function stopPageHotkeys(e) {
      // Stop up/down/backspace/space from moving the page
      if(e.which == 38 || e.which == 40 || e.which == 8 || e.which == 32) {
        e.preventDefault();
      }
    }
    
    // go up/down using arrows or attempt to autocomplete based on string
    function keydownSB(e) {
      if(e.altKey || e.ctrlKey) return false;
      var $selected = $items.filter(".selected");
      switch(e.which) {
      case 35: // end
        if($selected.size() > 0) {
          e.preventDefault();
          selectItem.call($items.not(".disabled").filter(":last")[0]);
          centerOnSelected();
        }
        break;
      case 36: // home
        if($selected.size() > 0) {
          e.preventDefault();
          selectItem.call($items.not(".disabled").filter(":first")[0]);
          centerOnSelected();
        }
        break;
      case 38: // up
        if($selected.size() > 0) {
          if($items.not(".disabled").filter(":first")[0] != $selected[0]) {
            e.preventDefault();
            selectItem.call($items.not(".disabled").eq($items.not(".disabled").index($selected)-1)[0]);
          }
          centerOnSelected();
        }
        break;
      case 40: // down
        if($selected.size() > 0) {
          if($items.not(".disabled").filter(":last")[0] != $selected[0]) {
            e.preventDefault();
            selectItem.call($items.not(".disabled").eq($items.not(".disabled").index($selected)+1)[0]);
            centerOnSelected();
          }
        }
        else if($items.size() > 1) {
          e.preventDefault();
          selectItem.call($items.eq(0)[0]);
        }
        break;
      default:
        break;
      }
    }
    function keyupSB(e) {
      if(e.altKey || e.ctrlKey) return false;
      var $selected = $items.filter(".selected");
      if(e.which != 38 && e.which != 40) {
        searchTerm += String.fromCharCode(e.keyCode);
        if(!selectMatchingItem(searchTerm)) {
          clearTimeout(cstTimeout);
          clearSearchTerm();
        }
        else {
          clearTimeout(cstTimeout);
          cstTimeout = setTimeout(clearSearchTerm, o.acTimeout);
        }
      }
    }
    
    // when the sb is focused (by tab or click), allow hotkey selection and kill all other selectboxes
    function focusSB() {
      killAllButMe();
      $sb.addClass("focused");
      $(document).unbind("keyup", keyupSB).keyup(keyupSB);
      $(document).unbind("keydown", stopPageHotkeys).keydown(stopPageHotkeys);
      $(document).unbind("keydown", keydownSB).keydown(keydownSB);
    }
    
    // when the sb is blurred (by tab or click), disable hotkey selection
    function blurSB() {
      $sb.removeClass("focused");
      $(document).unbind("keyup", keyupSB);
      $(document).unbind("keydown", stopPageHotkeys);
      $(document).unbind("keydown", keydownSB);
    }
    
    function addHoverState() { $(this).addClass("hover"); }
    function removeHoverState() { $(this).removeClass("hover"); }
    
    loadSB();
  });
};

})(jQuery);
