/* 
* vim: noexpandtab 
*/

/**
* JQuery UI combobox plugin.  This may be called on any element; the element
* is replaced by a text field and drop-down div.  If the replaced element was
* a select, the combobox options can be picked up from the contents of the
* select.  Otherwise, the 'data' option must be provided to specify choice.
*
* Method names in documentation are relative to the JQuery UI infrastructure,
* i.e. the method call is always 'combobox', and then the method name is
* passed as the first (string) argument.
*
* @fileoverview
* @author Jonathan Tang
* @dependency jquery-1.2.6.js
* @dependency ui.core.js
* @version 1.0.1
*/
; (function ($) {

    var KEY_UP = 38,
	KEY_DOWN = 40,
	KEY_ENTER = 13,
	KEY_ESC = 27,
	KEY_F4 = 115;

    $.widget('ui.combobox', {

        /**
        * Main JQuery method.  Call $(selector).combobox(options) on any element,
        * or collection of elements, to turn them into a combobox.
        * 
        * All event handlers take 2 arguments: the original browser event, and an
        * object with the following fields:<ul> 
        * <li>value: the current value of the input field</li>
        * <li>index: the index within the option list of the presently-selected
        * value, or -1 if directly inputted.</li>
        * <li>isCustom: true if the user has typed in an option not on the list</li>
        * <li>inputElement: JQuery object containing the input field</li>
        * <li>listElement: JQuery object containing the drop-down list</li>
        * </ul>
        * @function combobox
        * @param {Object} options Options hash
        * @option {Array<String>} data List of options for the combobox
        * 
        * @option {Boolean} autoShow If true (the default), then display the
        * drop-down whenever the input field receives focus.  Otherwise, the 
        * user must explicitly click the drop-down icon to show the list.
        * 
        * @option {Boolean} matchMiddle If true (the default), then the combobox
        * tries to match the typed text with any portion of any of the 
        * options, instead of just the beginning.
        * 
        * @option {Function(e, ui)} key Event handler called whenever a key is
        * pressed in the input box.
        * 
        * @option {Function(e, ui)} change Event handler called whenever a new
        * option is selected on the drop-down list (eg. down/up arrows, typing in 
        * the input field).
        * 
        * @option {Function(e, ui)} select Event handler called when a selection
        * is finished (enter pressed or input field loses focus)
        * 
        * @option {String} arrowUrl URL of the image used for the drop-down arrow.
        * Used only by the default arrowHTML function; if you override 
        * that, you don't need to supply this.  Defaults to "drop_down.png"
        * 
        * @option {Function()} arrowHTML Function that should return the HTML of
        * the element used to display the drop-down.  Defaults to an image tag.
        *
        * @option {String} listContainerTag Tag to hold the drop-down list element.
        *
        * @option {Function(String, Int)} listHTML Function that takes the option
        * datum and index within the list and returns an HTML fragment for each
        * option.  Default is a span of class ui-combobox-item.
        */

        options: {
            data: [],
            autoShow: true,
            matchMiddle: true,
            change: function (e, ui) { },
            select: function (e, ui) { },
            key: function (e, ui) { },
            arrowURL: '',
            arrowHTML: function () {
                return $('<div class="' + this.options.arrowClass + '"><img Width="'+this.options.arrowWidth+'" Height="'+this.options.arrowHeight+'" border = "0" src = "'
				+ this.options.arrowURL + '" /></div>')
            },
            listContainerTag: 'div',
            listHTML: defaultListHTML,
            readOnly: true,
            tabIndex: 1,
            arrowClass: "",
            arrowHeight: 22,
            arrowWidth: 18,
            inputClass: "",
            enableInputKeys: false,
            top: 3
        },
        _create: function () {
            var that = this;
            var options = this.options;

            var inputElem = $('<input type = "text" class="' + this.options.inputClass + '" tabindex="' + this.options.tabIndex + '" />');

            this.selectedIndex = -1;

            this.options.inputElem = inputElem;

            if (this.options.readOnly) {
                inputElem.attr("readonly", true);

            }
            if (this.element[0].tagName.toLowerCase() == 'select') {
                fillDataFromSelect(options, this.element);
            }

            function closeListOnDocumentClick() {
                that.hideList();
                $(document).unbind('click', closeListOnDocumentClick);
            };

            this.arrowElem = this.options.arrowHTML.call(this)
			.click(function (e) {
			    if (that.isListVisible()) {
			        that.hideList();
			    } else {
			        that.showList();
			        $(document).click(closeListOnDocumentClick);
			    }
			    return false;
			});

            function maybeCopyAttr(name, elem) {
                var val = that.element.attr(name);
                if (val) {
                    if (name == 'class') {
                        elem.addClass(val);
                    } else {
                        elem.attr(name, val);
                    }
                }
            };

            maybeCopyAttr('class', inputElem);
            maybeCopyAttr('name', inputElem);
            maybeCopyAttr('title', inputElem);
            maybeCopyAttr('dir', inputElem);
            maybeCopyAttr('lang', inputElem);
            maybeCopyAttr('xml:lang', inputElem);

            maybeCopyAttr('size', inputElem);
            maybeCopyAttr('value', inputElem);

            // Maxlength comes back -1 if unset, which causes problems when set
            if (this.element.attr('maxlength') != -1) {
                inputElem.attr('maxlength', this.element.attr('maxlength'));
            }

            this.oldElem = this.element
			.unbind('getData.combobox')
			.unbind('setData.combobox')
			.unbind('remove')
			.after(this.arrowElem)
			.after(inputElem)
			.remove();
            this.listElem = this.buildList().insertAfter(this.arrowElem).hide();

            // ID copied afterwards so we never have two elements with the same
            // ID in the DOM.
            maybeCopyAttr('id', inputElem);
            maybeCopyAttr('class', this.listElem);
            maybeCopyAttr('class', this.arrowElem);

            this.element = inputElem
			.keyup(function (e) {
			    if (e.which == KEY_F4) {
			        that.showList(e);
			    }

			})
			.change(boundCallback(this, 'fireEvent', 'select'))
            .keydown(boundCallback(this, 'inputKeyHandler'));

            if (options.autoShow) {
                this.element
				.focus(boundCallback(this, 'showList'))
				.blur(function (e) {
				    that.finishSelection(that.selectedIndex, e);
				    that.hideList();
				});
            }

        },

        cleanup: function () {
            // Cleanup and destroy are split into two separate handlers because
            // one of them (cleanup, in this case) needs to be bound to the
            // 'remove' event handler to clean up the extra elements.  The
            // destroy handler removes the custom input element added by this
            // plugin, and so we get an infinite loop if they aren't split.
            if (this.boundKeyHandler) {
                $(document).unbind('keyup', this.boundKeyHandler);
            }
            this.arrowElem.remove();
            this.listElem.remove();
        },

        /**
        * Remove all combobox functionality from this element, restoring the
        * original element.
        */
        destroy: function () {
            var newElem = this.element;
            this.element = this.oldElem.insertBefore(this.arrowElem);
            newElem.remove(); // Triggers cleanup
        },

        /**
        * Dynamically changes one of the combobox options.
        * 
        * @param {String} key Option name.
        * @param {Object} value New value.
        */
        _setOptions: function (key, value) {
            this.options[key] = value;

            if (key == 'disabled' && this.isListVisible()) {
                this.hideList();
            }

            if (key == 'data' || key == 'listContainerTag' || key == 'listHTML') {
                var isVisible = this.isListVisible();
                this.listElem = this.buildList().replaceAll(this.listElem);
                this[isVisible ? 'showList' : 'hideList']();
            }
        },

        buildList: function () {
            var that = this;
            var options = this.options;
            var tag = options.listContainerTag;
            var elem = $('<' + tag + ' class = "ui-combobox-list">' + '</' + tag + '>');

            $.each(options.data, function (i, val) {
                $(options.listHTML(val, i))
				.appendTo(elem)
				.click(boundCallback(that, 'finishSelection', i))
				.mouseover(boundCallback(that, 'changeSelection', i));
            });
            return elem;
        },

        isListVisible: function () {
            return this.listElem.is(':visible');
        },

        /**
        * Programmatically shows the drop-down list.
        * 
        * @param {Event} e Original event triggering this.
        */
        showList: function (e) {
            if (this.options.disabled) {
                return;
            }

            var pos = this.element.position();

            var listTop = this.options.top;

            var left = pos.left;
            var top = pos.top +this.element.height() + parseInt(listTop);
            var width = this.element.width() +10;

            $('.ui-combobox-list').css('border', '1px solid #7f9db9');
            this.listElem.css('left', left);
            this.listElem.css('top', top);
            this.listElem.css('width', width);

            this.boundKeyHandler = boundCallback(this, 'keyHandler');
            $(document).keyup(this.boundKeyHandler);
            $('.ui-combobox-list').hide();
            this.listElem.show();
            this.changeSelection(this.findSelection(), e);
        },

        /**
        * Programmatically hide the drop-down list.
        */
        hideList: function () {
            this.listElem.hide();
            $(document).unbind('keyup', this.boundKeyHandler);
        },

        keyHandler: function (e) {
            if (this.options.disabled) {
                return;
            }

            var optionLength = this.options.data.length;
            switch (e.which) {
                case KEY_ESC:
                    this.hideList();
                    break;
                case KEY_UP:
                    // JavaScript modulus apparently doesn't handle negatives
                    var newIndex = this.selectedIndex - 1;
                    if (newIndex < 0) {
                        newIndex = optionLength - 1;
                    }
                    this.changeSelection(newIndex, e);
                    break;
                case KEY_DOWN:
                    this.changeSelection((this.selectedIndex + 1) % optionLength, e);
                    break;
                case KEY_ENTER:
                    this.finishSelection(this.selectedIndex, e);
                    break;
                default:
                    this.fireEvent('key', e);
                    this.changeSelection(this.findSelection());
                    break;
            }
        },

        inputKeyHandler: function (e) {
            if (!this.options.enableInputKeys) return;
            var optionLength = this.options.data.length;
            switch (e.which) {
                case KEY_UP:
                    // JavaScript modulus apparently doesn't handle negatives
                    var newIndex = this.selectedIndex - 1;
                    if (newIndex < 0) {
                        newIndex = optionLength - 1;
                    }
                    this.element.val(this.options.data[newIndex]);
                    this.selectedIndex = newIndex;
                    break;
                case KEY_DOWN:
                    this.element.val(this.options.data[(this.selectedIndex + 1) % optionLength]);
                    this.selectedIndex = (this.selectedIndex + 1) % optionLength;
                    break;
            }

        },

        prepareCallbackObj: function (val) {

            val = val || this.element.val();
            var index = $.inArray(val, this.options.data);
            return {
                value: val,
                index: index,
                isCustom: index == -1,
                inputElement: this.element,
                listElement: this.listElement
            };
        },

        fireEvent: function (eventName, e, val) {
            var ui = this.prepareCallbackObj(val);
            this._trigger(eventName, 0, [ui]);

        },

        findSelection: function () {
            var data = this.options.data;
            var typed = this.element.val().toLowerCase();

            for (var i = 0, len = data.length; i < len; ++i) {
                var index = data[i].toLowerCase().indexOf(typed);
                if (index == 0) {
                    return i;
                }
            };

            if (this.options.matchMiddle) {
                for (var i = 0, len = data.length; i < len; ++i) {
                    var index = data[i].toLowerCase().indexOf(typed);
                    if (index != -1) {
                        return i;
                    }
                };
            }

            return 0;
        },

        changeSelection: function (index, e) {
            this.selectedIndex = index;
            this.listElem.children('.selected').removeClass('selected');
            this.listElem.children(':eq(' + index + ')').addClass('selected');
            if (e) {
                //this.fireEvent('change', e, this.options.data[index]);
            }
            //this.element.val(this.options.data[index]);
        },

        finishSelection: function (index, e) {
            this.element.val(this.options.data[index]);
            this.hideList();
            this.fireEvent('select', e, this.options.data[index]);

        }

    });

    function defaultListHTML(data, i) {
        var cls = i % 2 ? 'odd' : 'even';
        return '<div class = "ui-combobox-item ' + cls + '">' + data + '</div>';
    };

    function boundCallback(that, methodName) {
        var extraArgs = [].slice.call(arguments, 2);
        return function () {
            that[methodName].apply(that, extraArgs.concat([].slice.call(arguments)));
        };
    };

    function fillDataFromSelect(options, element) {
        var optionMap = {}, computedData = [];
        element.children().each(function (i) {
            if (this.tagName.toLowerCase() == 'option') {
                var text = $(this).text(),
				val = this.getAttribute('value') || text;
                optionMap[val] = text;
                computedData.push(val);
            }
        });

        if (!options.data.length) {
            options.data = computedData;
        }

        if (options.listHTML == defaultListHTML) {
            options.listHTML = function (data, i) {
                return defaultListHTML(optionMap[data] || data);
            };
        }
    };

})(jQuery);
