/**
 * Empfehlungs-/Autocomplete-Widget fuer jQuery
 * unterstuetzt mehrere Gruppen von Empfehlungen
 * 
 * created by BTD newmedia
 * 
 * @author sd
 */

/**
 * Erstellt ein Empfehlungswidget (Autocompleter) fuer ein Eingabefeld
 */
(function($) {
  $.fn.suggest = function(options) {
    return this.each(function() {        
        //Globale Variablen
        var input = $(this);
        var numberOfResults;
        var box;
        var selectedIndex;
        
        //Standardeinstellungen laden
        var o = $.extend({
            width: 'auto',                              //Breite der kompletten Box
            offsetTop: 0,                               //Versatz nach unten
            offsetLeft: 0,                              //Versatz nach rechts
            minChars: 1,                                //Anzahl Zeichen, welche noetig sind, bevor eine Empfehlung angezeigt wird
            created: function(object) { },              //Funktion, welche nach Erstellung des Containers getriggert wird
            groups: [{}],                               //Empfehlungsgruppen
            theme: {}                                   //Layout der Box
        }, options);
        o.theme = $.extend({
            containerClass: 'suggestContainer'          //CSS-Klasse der Empfehlungsbox
        }, o.theme);
        for (var i in o.groups) {
            o.groups[i] = $.extend({
                source: [],                             //Quelle fuer die Suche der Empfehlungen (Array oder URL)
                title: '',                              //Label der Gruppen
                autoSubmit: true,                       //Bei Auswahl einer Empfehlung, das Formular absenden
                highlight: true,                        //Suchbegriff hervorheben
                maxEntries: 10,                         //Maximale Anzahl an Suchergebnissen
                searchMode: 'normal',                   //Suchmodus (normal: Suchbegriff kann ueberall vorkommen)
                formatFunction: function(entry, searchterm){ return entry.value; }, //Funktion, welche die Ausgabe der Ergebnisse manipulieren kann
                applyFunction: function(entry){ input.val(entry.text()); },         //Funktion, welche die aktuelle Auswahl in das Eingabefeld laedt
                submitFunction: function(entry){ input.closest('form').submit(); }, //Funktion, welche das Formular absendet
                rankFunction: function(entry, rank){ return rank; },                //Funktion, welche das Ranking ermittelt
                created: function(object) { },          //Funktion, welche nach Erstellung der Gruppe getriggert wird
                entryCreated: function(object) { },     //Funktion, welche nach Erstellung eines Eintrages getriggert wird
                theme: {}                               //Layout der Gruppe
            }, o.groups[i]);
            o.groups[i].theme = $.extend({
                groupContainerClass: 'groupContainer',  //CSS-Klasse der Gruppenbox
                entryClass: 'entry',                    //CSS-Klasse der einzelnen Eintraege
                selectedClass: 'selected',              //CSS-Klasse des ausgewaehlten Eintrages
                highlightClass: 'highlight'             //CSS-Klasse der hervorgehobenen Suchbegriffe
            }, o.groups[i].theme);
        }
        
        /**
         * Funktion zum Initialisieren
         */
        var init = function() {
            numberOfResults = 0;
            selectedIndex = -1;
            
            //Position des Eingabefeldes ermitteln
            var position = input.position();
            
            //Container-Grundgeruest erstellen und positionieren
            box = $('<div></div>')
                .addClass(o.theme.containerClass)
                .hide()
                .css('position', 'absolute')
                .css('top', position.top + input.height() + parseInt(input.css('margin-top')) + parseInt(input.css('padding-top')) + o.offsetTop)
                .css('left', position.left + parseInt(input.css('margin-left')) + parseInt(input.css('padding-left')) + o.offsetLeft)
                .css('width', o.width == 'auto' ? input.width() : o.width);
            //Event triggern
            o.created(box);
            //Gruppencontainer erstellen
            var i = 0;
            $(o.groups).each(function() {
                var group = $('<fieldset><ul></ul></fieldset>')
                    .addClass(this.theme.containerClass)
                    .addClass('suggestGroup' + i)
                    .addClass('suggestGroup');
                //Label einfuegen
                if (this.title != '')
                    group.prepend('<legend>' + this.title + '</legend>');
                //Event triggern
                this.created(group);
                box.append(group);
                i++;
            });
            
            //Container einbetten
            input.after(box);
            
            //Standard-Autocompleter deaktivieren
            input.attr('autocomplete', 'off');
            
            //Events binden
            //Enter-Betaetigung behandeln
            input.keypress(function(event) {
                switch(event.keyCode) {
                    case 13: //Absenden (Enter)
                        return navigate(event);
                    case 40: //Runter (Pfeil nach unten)
                    case 38: //Hoch (Pfeil nach oben)
                    default: //Suchvorschlaege aktualisieren
                        return true;
                }
            });
            //Pfeiltasten und Suchbegriffeingabe behandeln
            input.keyup(function(event) {
                switch(event.keyCode) {
                    case 13: //Absenden (Enter)
                        return true;
                    case 40: //Runter (Pfeil nach unten)
                    case 38: //Hoch (Pfeil nach oben)
                        return navigate(event);
                    default: //Suchvorschlaege aktualisieren
                        find(input.val());
                        return true;
                }
            });
            //Pfeiltasten und Entertaste behandeln
            box.keyup(function(event) {
                return navigate(event);
            });
            //Wenn Mauszeiger den Container verlaesst, Markierung loeschen
            box.mouseout(function() {
                selectElement(-1);
            });
        }
        
        /**
         * Funktion zum Ausfuehren der Suche
         *
         * @param string text Suchbegriff
         */
        var find = function(text) {
            var i = 0;
            //Anzahl Ergebnisse auf 0 setzen und alle Gruppen ausblenden
            numberOfResults = 0;
            box.find('.suggestGroup').hide();
            //Gruppen verarbeiten
            $(o.groups).each(function() {
                var group = this;
                //Minimale Zeichenanzahl erreicht?
                if (text.length < o.minChars) {
                    processResult(i, [], text);
                } else {
                    if( typeof(group.source) != 'string') {
                        var results = [];
                        var results_rank = [[],[],[],[],[],[],[],[],[],[],[],[]];
                        //Quelle ist Feld
                        for (key in group.source) {
                            //Wenn Feld nur Strings enthält, den Eintrag in ein Objekt wandeln
                            if (typeof(group.source[key]) != 'object') {
                                entry = {keyword: group.source[key], value: group.source[key]};
                            } else {
                                //Objekt klonen
                                entry = $.extend(true, {}, group.source[key]);
                            }
                            //Suchfilter ausfuehren
                            entry.rank = group.rankFunction(entry, filter(entry.keyword, text, group.searchMode));
                            //Je nach Filterergebnis den Eintrag in das Ergebnisfeld einhängen
                            results_rank[entry.rank].push(entry);
                        }
                        
                        //Ergebnisse der verschiedenen Rankings zusammenfassen
                        results = results.concat(results_rank[10], results_rank[9], results_rank[8], results_rank[7], results_rank[6], results_rank[5], results_rank[4], results_rank[3], results_rank[2], results_rank[1]);
                        
                        //Ergebnisliste verarbeiten (ausgeben)
                        processResult(i, results, text);
                    } else {
                        //Quelle ist URL
                        $.getJSON(
                            group.source, 
                            {
                                filter: text
                            },
                            function(results) {
                                //Ergebnisliste verarbeiten (ausgeben)
                                processResult(i, results, text);
                            }
                        );
                    }
                }
                i++;
            });
            //Beim Bewegen der Maus ueber einen Eintrag, diesen markieren
            box.find('.entry').mousemove(function() {
                element = $(this);
                var entry = getEntryIndex(element);
                selectElement(entry);
            });
        }
        
        /**
         * Funktion zum Filtern
         *
         * @param string haystack Text, welcher durchsucht werden soll
         * @param string needle Suchbegriff
         * @param string mode Suchmodus (normal: Beginn mit Suchbegriff wird priorisiert, begins: nur Elemente die mit Suchbegriff beginnen, werden zugelassen, any: keine Priorisierung oder Einschraenkung)
         * @return int Ranking (0 - nicht gefunden, 1 - irgendwo gefunden, 2 - am Anfang eines Wortes gefunden, 3 - am Anfang gefunden)
         */
        var filter = function(haystack, needle, mode) {
            var result = haystack.toLowerCase().indexOf(needle.toLowerCase());
            switch(mode) {
                case 'normal': //Beginn mit Suchbegriff wird priorisiert
                    switch (result) {
                        case -1: //Nicht gefunden
                            return 0;
                        case 0: //Am Anfang gefunden
                            return 3;
                        default: //Irgendwo gefunden
                            //Testen, ob der Suchbegriff am Anfang eines Wortes steht
                            lastchar = haystack.substr(result - 1, 1)
                            if (lastchar == '-' || lastchar == ' ')
                                return 2;
                            return 1;
                    }
                case 'begins': //Nur Elemente die mit Suchbegriff beginnen, werden zugelassen
                    switch (result) {
                        case 0: //Am Anfang gefunden
                            return 1;
                        default: //Irgendwo oder gar nicht gefunden
                            return 0;
                    }
                case 'any': //Keine Priorisierung oder Einschraenkung
                default:
                    switch (result) {
                        case -1: //Nicht gefunden
                            return 0;
                        default: //Irgendwo gefunden
                            return 1;
                    }
            }
        }
        
        /**
         * Funktion zum Verarbeiten der Suchergebnisse
         *
         * @param int group_key Schluessel der Gruppe
         * @param array results Ergebnisfeld
         * @param string needle Suchbegriff
         */
        var processResult = function(group_key, results, text) {
            //Gruppencontainer suchen
            var group = box.find('.suggestGroup' + group_key);
            var numberOfGroupResults = 0;
            //Leeren
            var group_entries = group.find('ul');
            group_entries.html('');
            //Ergebnisse einhaengen
            for (result_key in results) {
                var result = results[result_key];
                //Ergebnis highlighten
                if (o.groups[group_key].highlight)
                    result.value = highlight(result.value, text, o.groups[group_key].theme.highlightClass);
                //Eintrag erstellen
                var entry = $('<li></li>')
                    .addClass(o.groups[group_key].theme.entryClass)
                    .html(o.groups[group_key].formatFunction(result, text))
                    .click(function () {
                        applyElement();
                    })
                    .keyup(function (event) {
                        //Enter gedrueckt
                        if (event.keyCode == 13) {
                            applyElement();
                        }
                    });
                //Event triggern
                o.groups[group_key].entryCreated(entry);
                group_entries.append(entry);
                
                //Maximale Anzahl an Eintraegen erreicht
                if (o.groups[group_key].maxEntries > 0 && numberOfGroupResults + 1 >= o.groups[group_key].maxEntries)
                    break;
                
                numberOfResults++;
                numberOfGroupResults++;
            }
            //Gruppe anzeigen, wenn sie Ergebnisse enthaelt
            if (results.length > 0)
                group.show();
            //Wenn die Anzahl aller Ergebnisse groesser 0 ist, den Empfehlungscontainer einblenden, sonst ausblenden
            if (numberOfResults > 0) {
                box.show();
            } else {
                box.hide();
            }
        }
        
        /**
         * Waehlt einen Eintrag aus und hebt ihn hervor, alle anderen Hervorhebungen werden entfernt
         *
         * @param int entry Eintragsnummer (-1 fuer keinen Eintrag)
         */
        var selectElement = function(entry) {
            counter = [];
            var group_key = 0;
            var i = 0;
            //Gruppen verarbeiten (Eintraege zaehlen und Markierung entfernen)
            $(o.groups).each(function() {
                var group = this;
                box.find('.entry').removeClass(group.theme.selectedClass);
                counter.push(box.find('.suggestGroup' + i + ' .entry').length);
                i++;
            });
            selectedIndex = entry;
            if (entry >= 0) {
                //Gruppen ueberspringen
                for (var i = 0; i < counter.length; i++) {
                    if (entry >= counter[i]) {
                        entry -= counter[i];
                    } else {
                        group_key = i;
                        break;
                    }
                }
                //Markierung setzen
                entries = box.find('.suggestGroup' + group_key + ' .entry');
                if (entry >= entries.length)
                    entry = entries.length - 1;
                $(entries[entry]).addClass(o.groups[group_key].theme.selectedClass);
            }
        }
        
        /**
         * Waehlt einen Eintrag aus und hebt ihn hervor, alle anderen Hervorhebungen werden entfernt
         *
         * @return boolean Ereignisrueckgabe (soll Ereignis normal verarbeitet werden)
         */
        var applyElement = function() {
            counter = [];
            var group_key = 0;
            var i = 0;
            //Gruppen verarbeiten (Eintraege zaehlen)
            $(o.groups).each(function() {
                counter.push(box.find('.suggestGroup' + i + ' .entry').length);
                i++;
            });
            entry = selectedIndex;
            if (entry >= 0) {
                //Gruppen ueberspringen
                for (var i = 0; i < counter.length; i++) {
                    if (entry >= counter[i]) {
                        entry -= counter[i];
                    } else {
                        group_key = i;
                        break;
                    }
                }
                //Wert in Eingabefeld übernehmen
                o.groups[group_key].applyFunction(box.find('.' + o.groups[group_key].theme.selectedClass));
                //Wenn automatisch Absenden aktiviert, dann Sende-Funktioni triggern
                if (o.groups[group_key].autoSubmit)
                    o.groups[group_key].submitFunction(box.find('.' + o.groups[group_key].theme.selectedClass));
                //Markierung aufheben
                selectElement(-1);
                //Eingabefeld fokussieren
                input.focus();
                //Empfehlungscontainer schliessen
                box.hide();
                //Ungewuenschtes Absenden verhindern
                return false;
            }
            return true;
        }
        
        /**
         * Funktion zum Ermitteln der Eintragsposition (Index)
         *
         * @param object entry Eintrag
         * @return int Position
         */
        var getEntryIndex = function(entry) {
            return box.find('.entry').index(entry);
        }
        
        /**
         * Funktion zum Handlen der Tastatureingaben
         *
         * @param object event Ereignis, welches vom Tastatur-Event kommt
         * @return bool Event weiter ausfuehren
         */
        var navigate = function(event) {
            switch(event.keyCode) {
                case 40: //Runter (Pfeil nach unten)
                    selectElement(selectedIndex + 1);
                    break;
                case 38: //Hoch (Pfeil nach oben)
                    selectElement(selectedIndex - 1);
                    break;
                case 13: //Absenden (Enter)
                    return applyElement();
                    break;
            }
            return true;
        }
        
        /**
         * Funktion zum Hervorheben des Suchbegriffes
         *
         * @param string haystack Originalzeichenkette
         * @param string needle Suchbegriff
         * @param string highlightClass Klasse, welche zum Hervorheben dienen soll
         * @return string Zeichenkette mit Hervorhebungen
         */
        var highlight = function(haystack, needle, highlightClass) {
            //needle escapen und mit dem regulaeren Ausdruck die Ersetzungen vornehmen
            return haystack.replace(new RegExp('(' + needle.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + ')', 'gi'), '<span class="' + highlightClass + '">$1<\/span>');
        }
        
        //Objekt initialisieren
        init();
    });
  }
})(jQuery);

