(function($) {
    function _arrayToVal( array, ele ) {
        var tags_array = [];
        $.each( array, function(key) {
            tags_array.push( key );
        });

        ele.val( tags_array.join(",") );
        ele.trigger("change");
    }

    // this doesn't act on the autocompletewithunknown widget. Instead, it's called on our input field with autocompletion
    function _handleComplete(e, ui, $oldElement) {
        var self = $(this).data("autocompletewithunknown");
        var ele = self.element;

        var curtagslist = self.cachemap[self.currentCache];

        var items = ui.item.value.toLowerCase();
        $.each( items.split(","), function() {
            var tag = $.trim(this);

            // check if the tag is empty, or if we had previously used it
            if ( tag == "" ) return;

            // TODO: don't put it in if the word exceeds the maximum length of a tag
            if ( ! self.tagslist[tag] ) {
                self.tagslist[tag] = true;


                var tokentype = curtagslist && curtagslist[tag] ? self.options.curTokenClass : self.options.newTokenClass;

                var $text = $("<span></span>")
                    .addClass(self.options.tokenTextClass)
                    .text(tag);
                var $a = $("<button class='fi-icon--with-fallback theme-color-on-hover'></button>").addClass(self.options.tokenRemoveClass).html("<span class='fi-icon fi-x' aria-hidden='true'></span>" +
                    "<span class='fi-icon--fallback'>Remove " + tag + "</span>");

                var $li = $("<li></li>").addClass(self.options.tokenClass + " " + tokentype)
                    .append($text).append($a).append(" ")
                    .attr( "title", tokentype == self.options.curTokenClass ? "tag: " + tag : "new tag: " + tag);
                if ( $oldElement )
                    $oldElement.replaceWith($li);
                else
                    $li.appendTo(self.uiAutocompletelist);
            }
        });

        $("#"+self.options.id+"_count_label").text("characters per tag:");
        $(this).val("");
        if ( self.options.grow ) {
            // shrink
            $(this).height((parseInt($(this).css('lineHeight').replace(/px$/,''), 10)||20) + 'px');
            if ( self.options.maxlength )
                $("#"+self.options.id+"_count").text(self.options.maxlength);
        }

        _arrayToVal(self.tagslist, ele)

        if ( $.browser.opera ) {
            var keyCode = $.ui.keyCode;
            if( e.which == keyCode.ENTER || e.which == keyCode.TAB )
                self.justCompleted = true;
        }

        // this prevents the default behavior of having the
        // form field filled with the autocompleted text.
        // Target the event type, to avoid issues with selecting using TAB
        if ( e.type == "autocompleteselect" ) {
            e.preventDefault();
            return false;
        }
    }

    $.widget("ui.autocompletewithunknown", {
            options: {
                id: null,
                tokenClass: "token",
                tokenTextClass: "token-text",
                tokenRemoveClass: "token-remove",
                newTokenClass: "new",
                curTokenClass: "autocomplete",
                numMatches: 20,
                populateSource: null, // initial location to populate from
                populateId: "",       // initial identifier for the list to be cached
                grow: false           // whether to automatically grow the autocomplete textarea or not
            },

            _filterTags: function( array, term ) {
                var self = this;
                var startsWithTerm = [];

                term = $.trim(term.toLowerCase());
                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term) );

                var filtered = $.grep( array, function(value) {
                            var val = value.label || value.value || value;
                            if ( !self.tagslist[val] && matcher.test( val ) )  {
                                if ( val.indexOf(term) == 0 ) {
                                    // ugly, but we'd like to handle terms that start with
                                    // in a different manner
                                    startsWithTerm.push({ value: val,
                                        label: val.replace(term, "<strong>"+term+"</strong>")});
                                    return false;
                                }
                                return true;
                            }
                        });

                // second step, because we first need to find out how many
                // total start with the term, and so need to be in the list
                // those that only contain the term fill in any remaining slots
                var responseArray = startsWithTerm.slice(0, self.options.numMatches);
                $.each(filtered, function(index, value) {
                    if ( responseArray.length >= self.options.numMatches )
                        return false;

                    responseArray.push({ value: value,
                        label: value.replace(term, "<strong>"+term+"</strong>")});
                })

                return responseArray;
            },

            _create: function() {
                var self = this;

                if (!self.options.id)
                    self.options.id = "autocomplete_"+self.element.attr("id");

                self.uiAutocomplete = self.element.wrap("<span class='autocomplete-container border'></span>").parent().attr("id", self.options.id).addClass(self.element.attr("class"));

                self.uiAutocompletelist = $("<ul class='autocomplete-list'></ul>").appendTo(self.uiAutocomplete).attr( "aria-live", "assertive" );

                // this is just frontend; will use JS to transfer contents to the original field (now hidden)
                self.uiAutocompleteInput = $("<input class='autocomplete-input' ></input>")
                        .appendTo(self.uiAutocomplete)
                        .data("autocompletewithunknown", self)
                        .focus(function(e) {
                            $(this).closest(".autocomplete-container").addClass("focus");
                        })
                        .blur(function(e) {
                            $(this).closest(".autocomplete-container").removeClass("focus");
                        });

                if ( self.options.grow ) {
                    self.uiAutocomplete.closest(".row").parent().find(".tagselector-controls")
                        .append("<div class='autocomplete-count-container right'><span id='"+self.options.id+"_count_label'>characters per tag:</span> <span id='"+self.options.id+"_count' class='autocomplete-count'>50</span></div>");
                    $(self.uiAutocompleteInput).vertigro(self.options.maxlength, self.options.id + "_count");
                }

                self.element.hide();

                self.tagslist = {};
                self.cache = {};
                self.cachemap = {};

                if ( self.options.populateSource )
                    self.populate(self.options.populateSource, self.options.populateId);

                self.uiAutocompleteInput.autocomplete({
                    source: function (request, response) {
                        if ( self.cache[self.currentCache] != null )
                            return response( self._filterTags( self.cache[self.currentCache], request.term ) );
                    },
                    autoFocus: true,
                    select: _handleComplete
                }).bind("keydown.autocompleteselect", function( event ) {
                    var keyCode = $.ui.keyCode;
                    var $input = $(this);

                    $("#"+self.options.id+"_count_label").text("characters left:");

                    switch( event.which ) {
                        case keyCode.ENTER:
                            _handleComplete.apply( $input,
                                    [event, { item: { value: $input.val() } } ]);
                            self.justCompleted = true;
                            $input.autocomplete("close");
                            event.preventDefault();

                            return;
                        case keyCode.TAB:
                            if ($input.val()) {
                                _handleComplete.apply( $input,
                                    [event, { item: { value: $input.val() } } ]);
                                self.justCompleted = true;
                                event.preventDefault();
                            }
                            return;
                        case keyCode.BACKSPACE:
                            if( ! $input.val() ) {
                                $("."+self.options.tokenRemoveClass + ":last", self.uiAutocomplete).focus();
                                event.preventDefault();
                            }
                            return;
                    }
                }).bind("keyup.autocompleteselect", function( event ) {
                    var $input = $(this);
                    if ( $input.val().indexOf(",") > -1 )
                        _handleComplete.apply( $input, [event, { item: { value: $input.val() } } ]);
                }).change(function(event){
                    // if we have the menu open, let that handle the autocomplete
                    var $menu = $(this).data("ui-autocomplete").menu;
                    if ( $menu.element.is(":visible") ) return;

                    _handleComplete.apply( $(this), [event, { item: { value: $(event.currentTarget).val() } } ]);

                    // workaround for autocompleting with TAB in opera
                    if ( $.browser.opera && self.justCompleted ) {
                        $(this).focus();
                        self.justCompleted = false;
                    }
                })
                .data("ui-autocomplete")._renderItem = function( ul, item ) {
                        return $( "<li></li>" )
                            .data( "ui-autocomplete-item", item )
                            .append( "<a>" + item.label + "</a>" )
                            .appendTo( ul );
                };


                // so other things can reinitialize the widget with their own text
                $(self.element).bind("autocomplete_inittext", function( event, new_text ) {
                    self.tagslist = {};
                    self.uiAutocompletelist.empty();
                    _handleComplete.apply( self.uiAutocompleteInput, [ event, { item: { value: new_text } } ] );
                });
                $(self.element).trigger("autocomplete_inittext", self.element.val());

                // replace one text
                $(self.element).bind("autocomplete_edittext", function ( event, $element, new_text ) {
                    _handleComplete.apply( self.uiAutocompleteInput, [ event, { item: { value: new_text } }, $element ] );
                });

                $(".autocomplete-container").click(function() {
                    self.uiAutocompleteInput.focus();
                });

                $("span."+self.options.tokenTextClass, self.uiAutocomplete.get(0))
                .live("click", function(event) {
                    delete self.tagslist[$(this).text()];
                    _arrayToVal(self.tagslist, self.element);
                    var $input = $("<input type='text' />")
                        .addClass(self.options.tokenTextClass)
                        .val($(this).text())
                        .width($(this).width()+5);
                    $(this).replaceWith($input);
                    $input.focus();

                    event.stopPropagation();
                });

                $("input."+self.options.tokenTextClass,self.uiAutocomplete.get(0))
                .live("blur", function(event) {
                    $(self.element).trigger("autocomplete_edittext", [ $(this).closest("li"), $(this).val() ] );
                });

                $("."+self.options.tokenRemoveClass, self.uiAutocomplete.get(0)).live("click", function(e) {
                    var $token = $(this).closest("."+self.options.tokenClass);

                    delete self.tagslist[$token.children("."+self.options.tokenTextClass).text()];
                    _arrayToVal(self.tagslist, self.element);
                    $token.fadeOut(function() {$(this).remove()});

                    e.preventDefault();
                    e.stopPropagation();
                }).live("focus", function(event) {
                    $(this).parent().addClass("focus");
                }).live("blur", function(event) {
                    $(this).parent().removeClass("focus");
                }).live("keydown", function(event) {
                    if ( event.which == $.ui.keyCode.BACKSPACE ) {
                        event.preventDefault();
                        var $prevToken = $(this).closest("."+self.options.tokenClass).prev("."+self.options.tokenClass);
                        $(this).click();

                        if ($prevToken.length == 1)
                            $prevToken.find("."+self.options.tokenRemoveClass).focus();
                        else
                            self.uiAutocompleteInput.focus();
                    } else if (event.which != $.ui.keyCode.TAB) {
                        $(this).siblings("."+self.options.tokenTextClass).click();
                    }
                });

                // workaround for autocompleting with ENTER in opera
                self.justCompleted = false;
                $.browser.opera && $(self.element.get(0).form).bind("submit.autocomplete", function(e) {
                    // this tries to make sure that we don't try to validate crossposting, if we only hit enter
                    // to autocomplete. Workaround for opera.
                    // Sort of like a lock, to mark which handler last prevented the form submission.
                    // TODO: refactor this out into something that we're sure works. We are at the mercy
                    // of the way that Opera and other browsers order the handlers.
                    if ( self.element.data("preventedby") == self.options.id)
                        self.element.data("preventedby", null)

                    if( self.justCompleted ) {
                        if ( ! self.element.data("preventedby") )
                            self.element.data("preventedby", self.options.id);
                        self.justCompleted = false;
                        return false;
                    }
                });
            },

            // store this in an array, and in a hash, so that we can easily look up presence of tags
            _cacheData: function(array, key) {
                this.currentCache=key;

                this.cache[this.currentCache] = array;
                this.cachemap[this.currentCache] = {};
                for( var i in array ) {
                    this.cachemap[this.currentCache][array[i]] = true;
                }
            },

            tagstatus: function(key) {
                var self = this;

                if ( !self.cachemap[key] )
                    return;

                // recheck tags status in case tags list loaded slowly
                // or we switched comms
                $("."+self.options.tokenClass, self.uiAutocomplete).each(function() {
                    var exists = self.cachemap[key][$(this).find("."+self.options.tokenTextClass).text()];
                    $(this).toggleClass(self.options.newTokenClass, !exists).toggleClass(self.options.curTokenClass, exists);
                });
            },

            populate: function(source, id) {
                var self = this;

                // Source is a URL endpoint we need to fetch
                if (typeof source === 'string' || source instanceof String) {
                    $.getJSON(source, function(data) {
                        if ( !data ) return;

                        self._cacheData(data.tags, id);
                        self.tagstatus(id);
                    });
                } else if (Array.isArray(source)) {
                // Source is a static array, just store it.
                    self._cacheData(source, id);
                    self.tagstatus(id);
                }
            },

            clear: function () {
                var self = this;
                self._cacheData([], "");
                self.tagstatus("");
            }
    });

})(jQuery);
