$.fn.isearch = function(options) {
    return this.each(function() {
        var $element=$(this).attr("isearch", "off");

        function failDataToUrl(data) { alert("Please set isearch failDataToUrl parameter"); return "http://wwww.jquery.com/"; }
        function failTransformResponse(data) { alert("Please set isearch failTransformResponse parameter"); return []; }
        function defaultFormatHeader(header) { return header; }
        function defaultFormatHit(header) { return hit; }
        function defaultOnSelect(value) { }

        var prefs={
            "dataToUrl"         : failDataToUrl,
            "transformResponse" : failTransformResponse,
            "formatHeader"      : defaultFormatHeader,
            "formatHit"         : defaultFormatHit,
            "onSelect"          : defaultOnSelect,
            "inputDelay"        : 50,
            "noResultsHTML"     : "No suggestions.",
            "topSelected"       : true,
            "padding"           : "4px"
        };
        $.extend(prefs, options);

        var hovering=false;

        var $results=$(document.createElement("div")).addClass("isearch-results").hide()
        .css({
            border: "1px solid black",
            position: "absolute"
        })
        .hover(
            function() { hovering = true;  },
            function() { hovering = false; }
        );
        $("body").append($results);

        var visibility={
            _visible : false,
            guaranteeVisible : function() {
                if(this.isVisible())
                    ; // Already showing the results, nothing to do
                else {
                    var pos=$element.position();
                    $results.css({
                        left:   pos.left+parseInt($element.css("margin-left"))+"px",
                        top:    pos.top+$element.outerHeight(true)+"px",
                        width:  $element.outerWidth()+"px"
                    }).show();
                    this._visible = true;
                }
            },
            guaranteeHidden : function() {
                if(this.isVisible()) {
                    this._visible = false;
                    $results.hide();
                }
                else
                    ; // Already hiding the results, nothing to do here
            },
            isVisible : function() {
                return this._visible;
            }
        };

        var selection={
            _selected: null,
            nudge: function(increment) {
                var hits=$(".isearch-hit"), selected0=this._selected, selectedp=selected0;
    
                if(!visibility.isVisible())
                    ; // Not showing results, nothing to do
                else
                if(hits.size() == 0)
                    ; // Results already visible, but none there, so nothing to do
                else
                if(selected0==null && increment>0)
                    selectedp = hits.get(0);
                else
                if(selected0==null && increment<0)
                    selectedp = hits.get(hits.size()-1);
                else {
                    var index=parseInt($(selected0).attr("hitno"))+increment;
                    if(index < 0)
                        index = 0;
                    else
                    if(index >= hits.size())
                        index = hits.size()-1;
                    selectedp = hits.get(index);
                }
    
                if(selected0==null || selectedp!=selected0) {
                    if(selected0 != null)
                        this.deselect(selected0);
                    this.select(selectedp);
                }
            },
            select : function(hit) {
                this.clear();
                this._selected = hit;
                $(hit).addClass("isearch-hit-selected");
            },
            deselect : function(hit) {
                this._selected = null;
                $(hit).removeClass("isearch-hit-selected");
            },
            clear : function() {
                if(this._selected != null)
                    this.deselect(this._selected);
            },
            execute : function() {
                var hit=content.get($(this._selected).attr("hitno"));
                this._selected = null;
                visibility.guaranteeHidden();
                $element.val("");
                $element.blur();
                content.clear();
                prefs.onSelect(hit);
            }
        };

        var content={
            _data : null,
            _hits : null,
            _hitno : 0,
            update : function() {
                if($element.val() == "") {
                    visibility.guaranteeHidden();
                    content.clear();
                }
                else {
                    var thisp=this;
                    $.get(prefs.dataToUrl($element.val()), function(response) {
                        selection.clear();

                        thisp._data = prefs.transformResponse(response);
    
                        var html=thisp._beginEmit();
                        if(thisp._data.length == 1)
                            html = thisp._emitHits(thisp._data[0].hits);
                        else {
                            html = "";
                            for(var index in thisp._data)
                                html = html+thisp._emitBucket(thisp._data[index]);
                        }
                        if(html == "")
                            html = prefs.noResultsHTML;
                        html = html+thisp._endEmit();
                        $results.html(html);
    
                        $(".isearch-bucket:first").css("padding-top",    prefs.padding);
                        $(".isearch-bucket:last").css("padding-bottom", prefs.padding);
                        $(".isearch-hit")
                        .hover(
                            function(e) {
                               selection.select(this);
                            },
                            function(e) {
                                selection.deselect(this);
                            }
                        )
                        .click(function(e){
                            selection.execute(e.target);
                        });

                        if(prefs.topSelected)
                            selection.select($(".isearch-hit:first").get(0));
                        visibility.guaranteeVisible();
                    });
                }
            },
            clear : function() {
                this._data = null;
                this._hits = null;
                $results.html("");
            },
            get : function(index) {
                return this._hits[index];
            },
            _emitHits : function(hits) {
                var result="";
                for(var index in hits) {
                    result = result+"<div style=\"text-indent: "+prefs.padding+"\" class=\"isearch-hit\" hitno=\""+this._hitno+"\">"+prefs.formatHit(hits[index])+"</div>";
                    this._hits.push(hits[index]);
                    this._hitno = this._hitno+1;
                }
                return result;
            },
            _emitBucket : function(bucket) {
                var result;
                if(bucket.hits.length == 0)
                    result = "";
                else
                    result = "<div class=\"isearch-bucket\"><div style=\"text-indent: "+prefs.padding+"\" class=\"isearch-header\">"+prefs.formatHeader(bucket.header)+"</div>"+this._emitHits(bucket.hits)+"</div>";
                return result;
            },
            _beginEmit : function() {
                this._hitno = 0;
                this._hits  = [];
                return "<div class=\"isearch-results\">";
            },
            _endEmit : function() {
                return "</div>";
            }
        };


        $element
        .focus(function() {
            content.update();
        })
        .blur(function(e) {
            if(hovering)
                ; // We probably lost focus by clicking on the results. Wait to get blurred by someone else.
            else {
                content.clear();
                visibility.guaranteeHidden();
            }
        })
        .keydown(function(e) {
            if(e.keyCode==9 || e.keyCode==13) { // tab || return
                selection.execute();
            } else
            if(e.keyCode == 38) { // up
                selection.nudge(-1);
            } else
            if(e.keyCode == 40) { // down
                selection.nudge(1);
            }
            else {
                setTimeout(function() {
                    content.update();
                }, prefs.inputDelay);
            }
        });
    });
};
