/**
 * JQuery Searchable DropDown Plugin
 *
 * @required jQuery 1.3.x
 * @author Sascha Wolski <hagman@gmx.de>
 * $Id: jquery.searchabledropdown.js 47 2010-04-07 08:57:05Z xhaggi $
 *
 * Based up on the AddIncSearch plugin published by Tobias Oetiker
 * http://plugins.jquery.com/project/AddIncSearch
 *
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
(function($) {

    // register plugin
    var plugin = register("searchable");

    // defaults
    plugin.defaults = {
        maxListSize: 100,
        maxMultiMatch: 50,
        exactMatch: false,
        wildcards: true,
        ignoreCase: true,
        warnMultiMatch: "top {0} matches ...",
        warnNoMatch: "no matches ...",
        latency: 200,
        zIndex: "auto",
        maxwidth: -1
    };

    /**
    * Execute function
    * element-specific code here
    * @param {Options} settings Settings
    */
    plugin.execute = function(settings, zindex) {

        // do not attach on IE6 or lower
        if ($.browser.msie && parseInt(jQuery.browser.version) < 7)
            return this;

        // only active select elements with drop down capability
        if (this.nodeName != "SELECT" || this.size > 1)
            return this;

        var self = $(this);
        var storage = { index: -1, options: null }; // holds data for restoring
        var idxAttr = "lang";
        var enabled = false;

        // detecting chrome
        $.browser.chrome = /chrome/.test(navigator.userAgent.toLowerCase());
        if ($.browser.chrome) $.browser.safari = false;

        // lets you override the options
        // inside the dom objects class property
        // requires the jQuery metadata plugin
        // <div class="hello {color: 'red'}">ddd</div>
        if ($.meta) {
            settings = $.extend({}, options, self.data());
        }

        // objects
        var wrapper = $("<div/>");
        var overlay = $("<div/>");
        var input = $("<input/>");
        var selector = $("<select/>");

        // matching option items
        var topMatchItem = $("<option>" + settings.warnMultiMatch.replace(/\{0\}/g, settings.maxMultiMatch) + "</option>").attr("disabled", "true");
        var noMatchItem = $("<option>" + settings.warnNoMatch + "</option>").attr("disabled", "true");


        var selectorHelper = {
            /**
            * Return DOM options of selector element
            */
            option: function(idx) {
                return $(selector.get(0).options[idx]);
            },
            /**
            * Returns the selected item of selector element
            */
            selected: function() {
                return selector.find(":selected");
            },
            /**
            * Get or Set the selectedIndex of the selector element
            * @param {int} idx SelectedIndex
            */
            selectedIndex: function(idx) {
                if (idx > -1)
                    selector.get(0).selectedIndex = idx;
                return selector.get(0).selectedIndex;
            },
            /**
            * Resize selector depends on the parameter size
            * @param {Number} size Size
            */
            size: function(size) {
                selector.attr("size", Math.max(2, Math.min(size, 20)));
            },
            /**
            * Reset the entries, which can be choose to it's inital state depends on selectedIndex and maxMultiMatch
            */
            reset: function() {
                // return if selector has data and stored index equal selectedIndex of source select element
                if ((self.get(0).selectedIndex - 1) == self.data("index"))
                    return;

                // calc start and length of iteration
                var idx = self.get(0).selectedIndex;
                var len = self.get(0).length;
                var mc = Math.floor(settings.maxMultiMatch / 2);
                var begin = Math.max(1, (idx - mc));
                var end = Math.min(len, Math.max(settings.maxMultiMatch, (idx + mc)));
                var si = idx - begin;

                // clear selector select element
                selector.empty();
                selectorHelper.size(end - begin);

                // append options
                for (var i = begin; i < end; i++)
                    selector.append($(self.get(0).options[i]).clone().attr(idxAttr, i - 1));

                // append top match item if length exceeds
                if (end > settings.maxMultiMatch)
                    selector.append(topMatchItem);

                // set selectedIndex of selector
                selector.get(0).selectedIndex = si;
            }
        };

        // draw it
        draw();

        /*
        * EVENT HANDLING
        */
        var suspendBlur = false;
        overlay.mouseover(function() {
            suspendBlur = true;
        });
        overlay.mouseout(function() {
            suspendBlur = false;
        });
        selector.mouseover(function() {
            suspendBlur = true;
        });
        selector.mouseout(function() {
            suspendBlur = false;
        });
        input.click(function(e) {
            if (!enabled)
                enable(e, true);
            else
                disable(e, true);
        });
        input.blur(function(e) {
            if (!suspendBlur && enabled)
                disable(e, true);
        });
        self.keydown(function(e) {
            if (e.keyCode != 9)
                enable(e, false, true);
        });
        self.click(function(e) {
            selector.focus();
        });
        selector.click(function(e) {
            if (selectorHelper.selectedIndex() < 0)
                return;
            disable(e);
        });
        selector.focus(function(e) {
            input.focus();
        });
        selector.blur(function(e) {
            if (!suspendBlur)
                disable(e, true);
        });
        selector.mousemove(function(e) {
            // Disabled on opera because of <select> elements always return scrollTop of 0
            // Affects up to Opera 10 beta 1, can be removed if bug is fixed
            // http://www.greywyvern.com/code/opera/bugs/selectScrollTop
            if ($.browser.opera && parseFloat(jQuery.browser.version) >= 9.8)
                return true;

            // get font-size of option
            var fs = Math.floor(parseFloat(/([0-9\.]+)px/.exec(selectorHelper.option(0).css("font-size"))));
            // calc line height depends on browser
            var fsdiff = 4;
            if ($.browser.opera)
                fsdiff = 2.5;
            if ($.browser.safari || $.browser.chrome)
                fsdiff = 3;
            fs += Math.round(fs / fsdiff);
            // set selectedIndex depends on mouse position and line height
            selectorHelper.selectedIndex(Math.floor((e.pageY - selector.offset().top + this.scrollTop) / fs));
        });

        // toggle click event on overlay div
        overlay.click(function(e) {
            input.click();
        });

        // trigger event keyup
        input.keyup(function(e) {

            // break searching while using navigation keys
            if (jQuery.inArray(e.keyCode, new Array(9, 13, 16, 33, 34, 35, 36, 38, 40)) > -1)
                return true;

            // set search text
            search = $.trim(input.val().toLowerCase());

            // if a previous timer is running, stop it
            clearSearchTimer();

            // start new timer
            timer = setTimeout(searching, settings.latency);
        });

        // trigger keydown event for keyboard usage
        input.keydown(function(e) {

            // return on shift, ctrl, alt key mode
            if (e.shiftKey || e.ctrlKey || e.altKey)
                return;

            // which key is pressed
            switch (e.keyCode) {
                case 9: // tab
                    disable(e);
                    moveTab(self, e.shiftKey ? -1 : 1);
                    break;
                case 13:  // enter
                    disable(e);
                    self.focus();
                    break;
                case 27: // escape
                    disable(e, true);
                    self.focus();
                    break;
                case 33: // pgup
                    if (selectorHelper.selectedIndex() - selector.attr("size") > 0) {
                        selectorHelper.selectedIndex(selectorHelper.selectedIndex() -= selector.attr("size"));
                    }
                    else {
                        selectorHelper.selectedIndex(0);
                    }
                    synchronize();
                    break;
                case 34: // pgdown
                    if (selectorHelper.selectedIndex() + selector.attr("size") < selector.get(0).options.length - 1) {
                        selectorHelper.selectedIndex(selectorHelper.selectedIndex() += selector.attr("size"));
                    }
                    else {
                        selectorHelper.selectedIndex(selector.get(0).options.length - 1);
                    }
                    synchronize();
                    break;
                case 38: // up
                    if (selectorHelper.selectedIndex() > 0) {
                        selectorHelper.selectedIndex(selectorHelper.selectedIndex() - 1);
                        synchronize();
                    }
                    break;
                case 40: // down
                    if (selectorHelper.selectedIndex() < selector.get(0).options.length - 1) {
                        selectorHelper.selectedIndex(selectorHelper.selectedIndex() + 1);
                        synchronize();
                    }
                    break;
                default:
                    return true;
            }

            // we handled the key.stop
            // doing anything with it!
            return false;
        });

        /**
        * Draw the needed elements
        */
        function draw() {

            // fix some styles
            self.css("text-decoration", "none");
            self.width(self.outerWidth());
            self.height(self.outerHeight());

            // wrapper styles
            wrapper.css("position", "relative");
            wrapper.css("width", self.outerWidth());
            // relative div needs an z-index (related to IE z-index bug)
            if ($.browser.msie)
                wrapper.css("z-index", zindex);

            // overlay div to block events of source select element
            overlay.css({
                "position": "absolute",
                "top": 0,
                "left": 0,
                "width": self.outerWidth(),
                "height": self.outerHeight(),
                "background-color": "#FFFFFF",
                "opacity": "0.01"
            });

            // overlay text field for searching capability
            input.attr("type", "text");
            input.hide();
            input.height(self.outerHeight());

            // default styles for text field
            input.css({
                "position": "absolute",
                "top": 0,
                "left": 0,
                "margin": "0px",
                "padding": "0px",
                "outline-style": "none",
                "border-style": "solid",
                "border-bottom-style": "none",
                "border-color": "transparent",
                "background-color": "transparent"
                //                "background-color": "red"
            });

            // copy selected styles to text field
            var sty = new Array();
            sty.push("border-left-width");
            sty.push("border-top-width");
            //sty.push("font-family");
            sty.push("font-size");
            sty.push("font-stretch");
            sty.push("font-variant");
            sty.push("font-weight");
            sty.push("color");
            sty.push("text-align");
            sty.push("text-indent");
            sty.push("text-shadow");
            sty.push("text-transform");
            sty.push("padding-left");
            sty.push("padding-top");
            for (var i = 0; i < sty.length; i++)
                input.css(sty[i], self.css(sty[i]));

            // adjust search text field
            // IE7
            if ($.browser.msie && parseInt(jQuery.browser.version) < 8) {
                input.css("padding", "0px");
                input.css("padding-left", "3px");
                input.css("border-left-width", "2px");
                input.css("border-top-width", "3px");
            }
            // chrome
            else if ($.browser.chrome) {
                input.height(self.innerHeight());
                input.css("text-transform", "none");
                input.css("padding-left", parseFloatPx(input.css("padding-left")) + 3);
                input.css("padding-top", 2);
            }
            // safari
            else if ($.browser.safari) {
                input.height(self.innerHeight());
                input.css("padding-top", 2);
                input.css("padding-left", 3);
                input.css("text-transform", "none");
            }
            // opera
            else if ($.browser.opera) {
                input.height(self.innerHeight());
                var pl = parseFloatPx(self.css("padding-left"));
                input.css("padding-left", pl == 1 ? pl + 1 : pl);
                input.css("padding-top", 0);
            }
            else if ($.browser.mozilla) {
                input.css("padding-top", "0px");
                input.css("border-top", "0px");
                input.css("padding-left", parseFloatPx(self.css("padding-left")) + 3);
            }
            // all other browsers
            else {
                input.css("padding-left", parseFloatPx(self.css("padding-left")) + 3);
                input.css("padding-top", parseFloatPx(self.css("padding-top")) + 1);
            }

            // adjust width of search field
            var offset = parseFloatPx(self.css("padding-left")) + parseFloatPx(self.css("padding-right")) +
    		parseFloatPx(self.css("border-left-width")) + parseFloatPx(self.css("border-left-width")) + 23;
            input.width(self.outerWidth() - offset);

            // store css width of source select object then set width
            // to auto to obtain the maximum width depends on the longest entry.
            // this is nessesary to set the width of the selector, because min-width
            // do not work in all browser.
            var w = self.css("width");
            var ow = self.outerWidth();
            self.css("width", "auto");
            var ow = ow > self.outerWidth() ? ow : self.outerWidth();
            self.css("width", w);

            if (settings.maxwidth > 0) { 
             ow = settings.maxwidth;
            }
           
            // entries selector replacement
            selector.hide();
            selectorHelper.size(self.get(0).length);
            selector.css({
                "position": "absolute",
                "top": self.outerHeight(),
                "left": 0,
                "width": ow,
                "border": "1px solid #333",
                "font-weight": "normal",
                "padding": 0,
                "background-color": self.css("background-color"),
                "text-transform": self.css("text-transform")
            });

            // z-index handling
            var zIndex = /^\d+$/.test(self.css("z-index")) ? self.css("z-index") : 1;
            // if z-index option is defined, use it instead of select element z-index
            if (settings.zIndex && /^\d+$/.test(settings.zIndex))
                zIndex = settings.zIndex;
            overlay.css("z-index", (zIndex).toString(10));
            input.css("z-index", (zIndex + 1).toString(10));
            selector.css("z-index", (zIndex + 2).toString(10));

            // append to container
            self.wrap(wrapper);
            self.after(overlay);
            self.after(input);
            self.after(selector);
        };

        /**
        * Enable the search facilities
        *
        * @param {Object} e Event
        * @param {boolean} s Show selector
        * @param {boolean} v Verbose enabling
        */
        function enable(e, s, v) {

            // exit event on disabled select element
            if (self.attr("disabled"))
                return false;

            // prepend empty option
            self.prepend("<option />");

            // set state to enabled
            if (typeof v == "undefined")
                enabled = !enabled;

            // reset selector
            selectorHelper.reset();

            // synchronize select and dropdown replacement
            synchronize();

            // store search result
            store();

            // show selector
            if (s)
                selector.show();

            // show search field
            input.show();
            input.focus();
            input.select();

            // select empty option
            self.get(0).selectedIndex = 0;

            if (typeof e != "undefined")
                e.stopPropagation();
        };

        /**
        * Disable the search facilities
        *
        * @param {Object} e Event
        * @param {boolean} rs Restore last results
        */
        function disable(e, rs) {

            // set state to disabled
            enabled = false;

            // remove empty option
            self.find(":first").remove();

            // clear running search timer
            clearSearchTimer();

            // hide search field and selector
            input.hide();
            selector.hide();

            // restore last results
            if (typeof rs != "undefined")
                restore();

            // populate changes
            populate();

            if (typeof e != "undefined")
                e.stopPropagation();
        };

        /**
        * Clears running search timer
        */
        function clearSearchTimer() {
            // clear running timer
            if (timer != null)
                clearTimeout(timer);
        };

        /**
        * Populate changes to select element
        */
        function populate() {
            // invalid selectedIndex or disabled elements do not be populate
            if (selectorHelper.selectedIndex() < 0 || selectorHelper.selected().get(0).disabled)
                return;

            // store selectedIndex
            self.get(0).selectedIndex = parseInt(selector.find(":selected").attr(idxAttr));

            // trigger change event
            self.change();

            // store selected index
            self.data("index", new Number(self.get(0).selectedIndex));
        };

        /**
        * Synchronize selected item on dropdown replacement with source select element
        */
        function synchronize() {
            if (selectorHelper.selectedIndex() > -1 && !selectorHelper.selected().get(0).disabled)
                input.val(selector.find(":selected").text());
            else
                input.val(self.find(":selected").text());
        };

        /**
        * Stores last results of selector
        */
        function store() {
            storage.index = selectorHelper.selectedIndex();
            storage.options = new Array();
            for (var i = 0; i < selector.get(0).options.length; i++)
                storage.options.push(selector.get(0).options[i]);
        };

        /**
        * Restores last results of selector previously stored by store function
        */
        function restore() {
            selector.empty();
            for (var i = 0; i < storage.options.length; i++)
                selector.append(storage.options[i]);
            selectorHelper.selectedIndex(storage.index);
            selectorHelper.size(storage.options.length);
        };

        /**
        * Move tab x steps forward or backward if you set negative values to step
        * @param {jQueryElement} jqe Element
        * @param {int} steps Steps to move tab forward or background if negative value
        * @return {Boolean} true if tab is moved, otherwise false
        */
        function moveTab(jqe, steps) {
            var fields = jqe.parents("form,body").eq(0).find("button,input[type!=hidden],textarea,select");
            var index = fields.index(jqe);
            if (index > -1 && index + steps < fields.length && index + steps >= 0) {
                fields.eq(index + steps).focus();
                return true;
            }
            return false;
        };

        /**
        * Escape regular expression string
        *
        * @param str String
        * @return escaped regexp string
        */
        function escapeRegExp(str) {
            var specials = ["/", ".", "*", "+", "?", "|", "(", ")", "[", "]", "{", "}", "\\", "^", "$"];
            var regexp = new RegExp("(\\" + specials.join("|\\") + ")", "g");
            return str.replace(regexp, "\\$1");
        };


        var timer = null;
        var searchCache;
        var search;

        /**
        * The actual searching gets done here
        */
        function searching() {
            if (searchCache == search) { // no change ...
                timer = null;
                return;
            }

            var matches = 0;
            searchCache = search;
            selector.hide();
            selector.empty();

            // escape regexp characters
            var regexp = escapeRegExp(search);
            // exact match
            if (settings.exactMatch)
                regexp = "^" + regexp;
            // wildcard support
            if (settings.wildcards) {
                regexp = regexp.replace(/\\\*/g, ".*");
                regexp = regexp.replace(/\\\?/g, ".");
            }
            // ignore case sensitive
            var flags;
            if (settings.ignoreCase)
                flags = "i";

            // RegExp object
            search = new RegExp(regexp, flags);

            // for each item in list
            for (var i = 1; i < self.get(0).length && matches < settings.maxMultiMatch; i++) {
                // search
                if (search.length == 0 || search.test(self.get(0).options[i].text)) {
                    var opt = $(self.get(0).options[i]).clone().attr(idxAttr, i - 1);
                    if (self.data("index") == i)
                        opt.text(self.data("text"));
                    selector.append(opt);
                    matches++;
                }
            }

            // result actions
            if (matches >= 1) {
                selectorHelper.selectedIndex(0);
            }
            else if (matches == 0) {
                selector.append(noMatchItem);
            }

            // append top match item if matches exceeds maxMultiMatch
            if (matches >= settings.maxMultiMatch) {
                selector.append(topMatchItem);
            }

            // resize selector
            selectorHelper.size(matches);
            selector.show();
            timer = null;
        };

        /**
        * Parse a given pixel size value to a float value
        * @param value Pixel size value
        */
        function parseFloatPx(value) {
            try {
                value = parseFloat(value.replace(/[\s]*px/, ""));
                if (!isNaN(value))
                    return value;
            }
            catch (e) { }
            return 0;
        };

        return;
    };

    /**
    * Register plugin under given namespace
    *
    * Plugin Pattern informations
    * The function creates the namespace under jQuery
    * and bind the function to execute the plugin code.
    * The plugin code goes to the plugin.execute function.
    * The defaults can setup under plugin.defaults.
    *
    * @param {String} nsp Namespace for the plugin
    * @return {Object} Plugin object
    */
    function register(nsp) {

        // init plugin namespace
        var plugin = $[nsp] = {};

        // bind function to jQuery fn object
        $.fn[nsp] = function(settings) {
            // extend default settings
            settings = $.extend(plugin.defaults, settings);

            var elmSize = this.size();
            return this.each(function(index) {
                plugin.execute.call(this, settings, elmSize - index);
            });
        };

        return plugin;
    };

})(jQuery);

