CR.Fluxbox = CR.Class({
    base: CR.Base,
    defaults: {
        panel_id:           'fluxbox',
        id_selector:        'input[type="hidden"]',
        input_selector:     'input[type="text"]',
        results_selector:   'div.results',
        result_selector:    'div.result',
//        container_class:    'fluxbox-container',
//        content_class:      'fluxbox-content'
        result_template:    '{name}',
        display_value:      'name',
        hidden_value:       'id',
        min_chars:          2,
        query_delay:        350,
        hide_delay:         200,
        fade_delay:         300,
        page_size:          10,
        container_class:    'ffb',
        match_class:        'match',
        result_class:       'result',
        select_class:       'select',
        click_class:        'click',
        no_results_class:   'result no_results',
        no_results_text:    'No Results'
    }
});

CR.Fluxbox.prototype.init = function(config) {
    var panel     = $(config.element);
    this.panel    = panel;
    this.panel_id = panel.attr('id') || config.panel_id;
    this.results  = panel.find( config.results_selector ).hide();
    this.url      = config.url;
    this.cache    = { };
    this.make_handlers(config);
    this.find_inputs(config);
    this.build_content(config);
    this.visible = 0;
};

CR.Fluxbox.prototype.make_handlers = function() {
    var self      = this;
    var panel     = this.panel;
    var config    = this.config;

    this.handlers = this.handlers || {
        hide: function() {
            self.results.empty();
            self.visible = 0;
            self.result  = undefined;
            self.rdata   = undefined;
        },
        hide_later: function() {
            if (self.pending_hide) {
                clearTimeout(self.pending_hide);
            }
            self.pending_hide = setTimeout( 
                function() { 
                    delete self.pending_hide;
                    self.handlers.hide();
                }, 200
            );
        },
        cancel_hide: function() {
            if (self.pending_hide) {
                clearTimeout(self.pending_hide);
            }
        },
        fade_out: function() {
            self.results.fadeOut(config.fade_delay, self.handlers.hide);
        },
        focus: function() {
            if (self.config.prompt && self.input.field.val() == self.config.prompt) {
                self.input.field.val('');
            }
        },
        blur: function() {
            self.handlers.hide_later();
        },
        got_results: function(data) {
            self.prepare_results(data);
            self.show_results(data);
        },
        keypress: function(e) {
            // taken more or less verbatim from flexbox.js
            // self.debug('keypress: %d', e.keyCode);

            // handle modifiers
            var mod = 0;
            if (typeof (e.ctrlKey) !== 'undefined') {
                if (e.ctrlKey)  mod |= 1;
                if (e.shiftKey) mod |= 2;
            } 
            else {
                if (e.modifiers & Event.CONTROL_MASK) mod |= 1;
                if (e.modifiers & Event.SHIFT_MASK)   mod |= 2;
            }
            // if the keyCode is one of the modifiers, bail out (we'll catch it on the next keypress)
            if (/16$|17$/.test(e.keyCode)) return; // 16 = Shift, 17 = Ctrl

            // look for tab or backspace
            var tab = e.keyCode === 9;
            var tabWithModifiers = e.keyCode === 9 && mod > 0;
            var backspace = e.keyCode === 8;

            // tab is a special case, since we want to bubble events...
            if (tab && self.result) {
//              self.debug('tab select');
                self.select_current();
            }

            if (tab || tabWithModifiers) {
                self.hide_results();
            }
            else if ( 
                // Up/down/escape/right arrow/left arrow requires results to 
                // be visible.  Enter/Down fire off the fluxbox if it's not 
                // currently visible
                (/^(?:27|33|34|38)$/.test(e.keyCode) && self.visible)
            ||	(/^(?:13|40)$/.test(e.keyCode))) {
                if (e.preventDefault) e.preventDefault();
                if (e.stopPropagation) e.stopPropagation();

                e.cancelBubble = true;
                e.returnValue = false;

                switch (e.keyCode) {
                    case 33: // page up
                        self.prev_result(self.config.page_size);
                        break;
                    case 34: // page down
                        self.next_result(self.config.page_size);
                        break;
                    case 38: // up
                        self.prev_result();
                        break;
                    case 40: // down
                        if (self.visible)       { self.next_result(); }
                        else                    { self.fluxbox(); }
                        break;
                    case 13: // enter
                        if (self.result)        { self.select_current(); }
                        else                    { self.fluxbox(); }
                        break;
                    case 27: //	escape
                        self.hide_results();
                        break;
                }
            } 
            else {
                self.fluxbox_delay(backspace);
            }
        },
        mouseover: function() {
	        self.result = $(this).solo_class(config.select_class);
		},
		prev_page_click: function(e) {
			e.preventDefault();
			e.stopPropagation();
			self.prev_page();
		},
		next_page_click: function(e) {
			e.preventDefault();
			e.stopPropagation();
			self.next_page();
		},
		click: function(e) {
		    self.handlers.cancel_hide();
			e.preventDefault();
			e.stopPropagation();
			self.select_current(self.result = $(this));
		}
    };
    return this.handlers;
};

CR.Fluxbox.prototype.find_inputs = function(config) {
    this.input = {
        id:     this.panel.find( config.id_selector ),
        field:  this.panel.find( config.input_selector ).attr('autocomplete', 'off')
    };
    this.input.field
        .focus(this.handlers.focus)
        .blur(this.handlers.blur);
    
    if (config.prompt) {
        this.input.field.val(config.prompt);
    }

    // cargo culted from flexbox.js
    if ($.browser.msie) {
        this.input.field.keydown(this.handlers.keypress);
    }
    else {
        this.input.field.keypress(this.handlers.keypress);
    }
};

CR.Fluxbox.prototype.build_content = function(config) {
    this.no_results_row = CR.HTML.element(
        'div', 
        {'class': config.no_results_class },
        config.no_results_text
    );
};

CR.Fluxbox.prototype.fluxbox_delay = function(longer) {
    var delay = this.config.query_delay;
    delay = longer ? delay * 3 : delay;
    this.delay_method( delay, this.fluxbox );
};

CR.Fluxbox.prototype.fluxbox = function() {
    var input = this.input.field;
    var query = input.val().trim();
    this.input.id.val('');      // clear any pre-defined id

    if (query.length >= this.config.min_chars) {
        if (this.cache[query]) {
//          this.debug('using cached results for: %s', query);
            this.show_results(this.cache[query]);
        }
        else {
//          this.debug('querying: %s q=%s', this.url, query);
            jQuery.getJSON(
                this.url,
                this.prepare_params({ q: query }),
                this.handlers.got_results
            );
        }
    }
    else if (query.length == 0) {
        this.handlers.fade_out();
    }
};

CR.Fluxbox.prototype.prepare_params = function(params) {
    if (this.config.on_prepare) {
        this.config.on_prepare(this, params);
    }
    return params;
};

CR.Fluxbox.prototype.prepare_results = function(data) {
    var config  = this.config;
    var results = data.results;
    var query   = data.query;

    // cache the data against the query
    this.cache[query] = data;
    
    // massage the results and generate a div row for each result
    for (var r in results) {
        var result = results[r];
        var text   = config.result_template.expand(result); 
        var exact  = query === text;

        if (query.length && ! exact) {
            text = text.replace(
                new RegExp(query, 'i'), 
                '<span class="' + config.match_class + '">' + query + '</span>'
            );
        }

//      this.debug('row %s [%s] [%s]', r, result[config.display_value], result[config.hidden_value]);
        
        result.row = $(document.createElement('div'))
            .attr('rel', r)
            .addClass(config.result_class)
            .html(text);
    	
        if (exact) {
            result.row.addClass(config.select_class);
        }
    }
};

CR.Fluxbox.prototype.show_results = function(data) {
    var self    = this;
    var config  = this.config;
    var total   = parseInt(data.total);
    var results = data.results;
    var query   = data.query;
    var n = 0;

//  this.debug('show_results()');

    if (! total) {
        this.no_results();
        return total;
    }
    
    if (this.rdata && this.rdata.query == query) {
//      this.debug('no change to results');
        return;
    }
    this.results.empty().show();
    this.rdata   = data;
    this.visible = 1;
    this.result  = undefined;

    var on_click = this.handlers.click;
    var on_mover = this.handlers.mouseover;
    
    for (var r in results) {
        var data  = results[r];
        this.results.append(
            data.row
            .click(on_click)
    		.mouseover(on_mover)
        );
    }

//	results.css('height', 'auto');

};

CR.Fluxbox.prototype.no_results = function() {
    this.results.empty().append(this.no_results_row).show();
    this.rdata  = undefined;
    this.result = undefined;
};

CR.Fluxbox.prototype.hide_results = function() {
    this.rdata  = undefined;
    this.result = undefined;
    this.handlers.fade_out();
};

CR.Fluxbox.prototype.select_current = function(that) {
    var row = that || this.result;
    if (! row) return;
    var config = this.config;
    if (this.rdata) {
        var result = this.rdata.results[row.attr('rel')];
        row.solo_class(config.click_class);
        this.select_result(row,result);
    }
    this.handlers.fade_out();
};

CR.Fluxbox.prototype.select_result = function(row, result) {
    var text = result[this.config.display_value];
    this.input.field.val(text).focus();
    this.input.id.val(result.id);
//  this.debug("set field to %s\nset id to %s", this.input.field.val(), this.input.id.val());
    if (this.config.on_select) {
        this.config.on_select(this, row, result);
    }
};

CR.Fluxbox.prototype.prev_result = function(page) {
    var row  = this.result;
    var prev = row
        ? row.prev()
        : this.results.find('div.' + this.config.result_class + ':last');

    if (prev.length <= 0) 
        return;
    
    if (page) {
        // yucky hack to roll backwards by page entries
        var p;
        while (--page && (p = prev.prev()) && p.length) {
            prev = p;
        }
    }

    this.result = prev.solo_class(this.config.select_class);
    this.check_bounds();
};

CR.Fluxbox.prototype.next_result = function(page) {
    var row  = this.result;
    var next = row
        ? row.next()
        : this.results.find('div.' + this.config.result_class + ':first');

    if (next.length <= 0)
        return;

    if (page) {
        // yucky hack to roll forwards by page entries
        var n;
        while (--page && (n = next.next()) && n.length) {
            next = n;
        }
    }

    this.result = next.solo_class(this.config.select_class);
    this.check_bounds();
};

CR.Fluxbox.prototype.check_bounds = function() {
    var pos = this.result.position();
    var scr = this.results.scrollTop();
    var n;
    
//  this.debug(
//      "OLD POS: top: %d  bot: %d  height: %d  scroll: %d  max height: %d", 
//      pos.top, pos.top + this.result.outerHeight(), this.result.outerHeight(), scr, this.results.innerHeight()
//  );
    
    if ((n = pos.top + this.result.outerHeight() - this.results.innerHeight()) > 0) {
        this.results.scrollTop(scr + n);
    }
    
    if (pos.top < 0 && scr > 0) {
        // pos.top is negative so add it to scrollTop reduces the scrolling,
        // i.e. moves the scroll bar up
        this.results.scrollTop(scr + pos.top);
    }
};



jQuery.fn.extend({
    fluxbox: function(config) {
        config = config || { };
        return this.each(
            function () {
                new CR.Fluxbox(
                    jQuery.extend({ element: this }, config)
                );
            }
        );
    }
});




