(function ($) {

    /*
     * Initialization of namespaces
     */

    window.XT = window.XT || {};
    var XT = window.XT;

    // NOTE: stolen from jquery.dimensions.js
    function num(el, prop) {
        return parseInt($.curCSS(el.jquery?el[0]:el,prop,true))||0;
    }

    var DEBUG = true;

    function log() {
        if (!DEBUG || !window.console) {
            return;
        }
        window.console.log.apply(window.console, arguments);
    }

    /****************************************************************************************************************/

    /**
     * Object Keys
     */

    var Keys = {
        TAB: 9,
        ENTER: 13,
        ESC: 27,
        PGUP: 33,
        PGDN: 34,
        END: 35,
        HOME: 36,
        UP: 38,
        DOWN: 40
    };
    XT.Keys = Keys;

    /****************************************************************************************************************/

    /**
     * Class DropDown
     *
     * @param options
     */

    function DropDown(options) {
        var defaultOptions = {
            top: null,
            left: null,
            width: null,
            minWidth: null,
            clickCallback: null,
            changeCallback: null,
            addClass: null
        };
        var opts = $.extend(defaultOptions, options);

        this.elem = $('<div class="dropdown"></div>');
        this.elem.hide();
        this.elem.css("position", "absolute");
        if (opts.addClass != null) {
            this.elem.addClass(opts.addClass);
        }

        if (opts.top != null) {
            this.elem.css("top", opts.top);
        }
        if (opts.left != null) {
            this.elem.css("left", opts.left);
        }

        if (opts.width != null) {
            this.elem.css("width", opts.width);
        }
        else if (opts.minWidth != null) {
            this.elem.css("min-width", opts.minWidth);
        }

        this.elem.appendTo($("body"));
        this.inputHelper = $('<input type="text"/>');
        this.inputHelper.hide();
        this.inputHelper.appendTo(this.elem);
        this.content = $('<div class="dropdown-content"></div>');
        this.content.appendTo(this.elem);

        this.entries = [];
        this.selectedIndex = -1;

        this.keypressHandler = null;
        this.skipKeys = 0;

        this.changeCallback = opts.changeCallback;
        this.clickCallback = opts.clickCallback;

        this.init();
    }
    XT.DropDown = DropDown;

    $.extend(DropDown.prototype, {
        init: function () {
        },

        show: function (options) {
            var defaultOptions = {
                top: null,
                left: null,
                width: null,
                minWidth: null
            }
            var opts = $.extend(defaultOptions, options);
            if (!this.empty()) {
                if (opts.top != null) {
                    this.elem.css("top", opts.top);
                }
                if (opts.left != null) {
                    this.elem.css("left", opts.left);
                }

                if (opts.width != null) {
                    this.elem.css("width", opts.width);
                }
                else if (opts.minWidth != null) {
                    this.elem.css("min-width", opts.minWidth);
                }

                this.elem.show();
            }
            return this;
        },

        hide: function () {
            this.elem.hide();
            return this;
        },

        empty: function () {
            return this.entries.length == 0;
        },

        updateSuggestions: function (entries, fillCallback) {
            var self = this;

            this.entries = entries || [];

            var hoverCallback = function () {
                self._hover($(this).parent().children().index(this));
            }

            var clickCallback = function (ev) {
                ev.stopPropagation();
                self._click($(this).parent().children().index(this));
            }

            this.content.text('');
            var list = $("<ul></ul>").addClass('dropdown').appendTo(self.content);
            $.each(entries, function (i, val) {
                var entryContainer = $("<li></li>").addClass('dropdown').appendTo(list).mouseover(hoverCallback).click(clickCallback);
                if (fillCallback) {
                    fillCallback.call(window, entryContainer, val);
                }
                else {
                    entryContainer.text(val);
                }
            });

            return this;
        },

        active: function () {
            return this.elem.is(":visible");
        },

        change: function () {
            // TODO: implement
            var selected = null;

            if (this.changeCallback != null) {
                this.changeCallback.call(window, selected);
            }
        },

        onchange: function (callback) {
            this.changeCallback = callback;
        },

        selected: function () {
            return this.selectedIndex >= 0 && this.selectedIndex < this.entries.length;
        },

        selection: function () {
            return this.selected() ? this.entries[this.selectedIndex] : null;
        },

        nextIndex: function (step) {
            if (!this.selected()) {
                return 0;
            }

            step = step || 1;

            var next = this.selectedIndex + step;
            if (next >= this.entries.length) {
                next = 0;
            }

            return next;
        },

        prevIndex: function (step) {
            if (!this.selected()) {
                return 0;
            }

            step = step || 1;

            var prev = this.selectedIndex - step;
            if (prev < 0) {
                prev = this.entries.length - 1;
            }

            return prev;
        },

        firstIndex: function () {
            return 0;
        },

        lastIndex: function () {
            return this.entries.length - 1;
        },

        select: function (index) {
            return this._hover(index, this.changeCallback);
        },

        highlight: function (index) {
            return this._hover(index);
        },

        _hover: function (index, changeCallback) {
            if (this.empty()) {
                return this;
            }

            if (index == null || index < 0 || index >= this.entries.length) {
                index = 0;
            }

            log("selecting entry " + index);

            var oldIndex = this.selectedIndex;

            this.deselect();

            var list = $("ul.dropdown li.dropdown", this.content);
            var sel = list.eq(index);
            sel.addClass("selected");

            var selPos = sel.position();
            if (selPos.top < 0) {
                this.content.scrollTop(this.content.scrollTop() + selPos.top);
            }
            else {
                var contentHeight = this.elem.innerHeight();
                var selHeight = sel.outerHeight();
                if (selPos.top + selHeight > contentHeight) {
                    var margin = num(sel, 'marginTop') + num(sel, 'marginBottom');
                    this.content.scrollTop(this.content.scrollTop() + selPos.top - contentHeight + selHeight + margin);
                }
            }

            if (index != oldIndex && changeCallback) {
                changeCallback.call(window, this.entries[index]);
            }

            this.selectedIndex = index;

            return this;
        },

        onclick: function (callback) {
            this.clickCallback = callback;
        },

        _click: function (index) {
            this._hover(index);
            this.hide();
            if (this.selectedIndex != -1 && this.clickCallback != null) {
                this.clickCallback.call(window, this.entries[index]);
            }
            return this;
        },

        deselect: function (index) {
            if (index == null || index < 0 || index >= this.entries.length) {
                index = this.selectedIndex;
            }

            $("ul.dropdown li.dropdown", this.content).eq(index).removeClass("selected");

            this.selectedIndex = -1;

            return this;
        },

        clear: function () {
            this.hide();
            this.entries = [];
            this.selectedIndex = -1;
        }
    })

    /****************************************************************************************************************/

    /**
     * Class AutoComplete
     *
     * @param input
     * @param searchCallback
     * @param loadCallback
     * @param changeCallback
     */

    function AutoComplete(input, options) {
        this.input = $(input);

        var defaultOptions = {
            searchCallback: null,
            searchTriggerCheck: null,
            textCallback: null,
            searchMinLength: 1,
            wildcards: null,
            loadCallback: null,
            changeCallback: null,
            fillCallback: null,
            dropDownClass: null,
            adjustDropDownWidth: false,
            forceDropDownWidth: false,
            selectPageStepSize: 5,
            suggestionSelect: null,
            eagerUpdate: false
        };
        var opts = $.extend(defaultOptions, options || {});

        this.searchCallback = opts.searchCallback;
        this.searchTriggerCheck = opts.searchTriggerCheck;
        this.textCallback = opts.textCallback;
        this.fillCallback = opts.fillCallback;
        this.wildcards = opts.wildcards;
        this.onLoad = opts.loadCallback;
        this.onChange = opts.changeCallback;
        this.minLength = opts.searchMinLength;
        this.dropDownClass = opts.dropDownClass;
        this.adjustDropDownWidth = opts.adjustDropDownWidth;
        this.forceDropDownWidth = opts.forceDropDownWidth;
        this.delay = 200;
        this.changeCount = 0;
        this.lastChanged = 0;
        this.selectPageStepSize = opts.selectPageStepSize;
        this.suggestionSelect = opts.suggestionSelect;
        this.init();
    }

    AutoComplete.dataName = "XT.AutoComplete";

    XT.AutoComplete = AutoComplete;

    $.extend(AutoComplete.prototype, {
        init: function () {
            var self = this;

            if (this.suggestionSelect == null) {
                var inputWidth = this.input.width();
                var ddOpts = {};
                if (this.forceDropDownWidth) {
//                    log("forcing drop down width:", inputWidth);
                    ddOpts.width = inputWidth;
                }
                else if (this.adjustDropDownWidth) {
//                    log("adjusting drop down width:", inputWidth);
                    ddOpts.minWidth = inputWidth;
                }

                if (this.dropDownClass != null) {
                    ddOpts.addClass = this.dropDownClass;
                }

                this.suggestionSelect = new DropDown(ddOpts);
            }

            if (this.searchTriggerCheck == null) {
                if (this.wildcards != null && !$.isArray(this.wildcards)) {
                    this.wildcards = ("" + this.wildcards).split('');
                }

                this.searchTriggerCheck = function (value) {
                    return self._remove_wildcards(value).length >= self.minLength;
                }
            }

            var keypressHandler = function (ev) {
                var propagate = true;

                log("got keypress event: key: ", ev.keyCode, "; event: ", ev);

                if (ev.which == Keys.ENTER
                    || ev.which == Keys.TAB || ev.keyCode == Keys.TAB
                    || ev.which == Keys.DOWN || ev.keyCode == Keys.DOWN
                    || ev.which == Keys.UP || ev.keyCode == Keys.UP
                    || ev.which == Keys.PGDN || ev.keyCode == Keys.PGDN
                    || ev.which == Keys.PGUP || ev.keyCode == Keys.PGUP
                    || ev.which == Keys.HOME || ev.keyCode == Keys.HOME
                    || ev.which == Keys.END || ev.keyCode == Keys.END
                    || ev.which == Keys.ESC || ev.keyCode == Keys.ESC) {
                    if (ev.which == 0) {
                        // FIXME: Investigate, when exactly is ev.which set, when ev.keyCode
                        ev.which = ev.keyCode;
                    }
                    if (!self.unchanged(ev)) {
                        // NOTE: This does not really stop propagation of the event
                        // NOTE: Returning false seems to work though. Should investigate issue further.
                        ev.stopPropagation();
                        propagate = false;
                    }
                    input.one('keyup', function () {
                        input.unbind('keypress', keypressHandler);
                    });
                }
                else {
                    input.one('keyup', function (ev) {
                        var oldValue = self.inputOldValue;
                        var newValue = input.attr("value");
                        if (oldValue == newValue) {
                            propagate = self.unchanged(ev);
                        }
                        else {
                            self.changed(ev);
                        }
                        input.unbind('keypress', keypressHandler);
                    });
                }

                return propagate;
            }
            var input = this.input;
            self.inputOldValue = null;
            input.keydown(function () {
                self.inputOldValue = input.attr("value");
                input.bind('keypress', keypressHandler)
            });

            // NOTE: deactivated, replaced by blur handling below
//            $(document).click(function () {
//                self.suggestionSelect.hide();
//            });
            input.click(function (ev) {
                ev.stopPropagation(); // don't hide suggestionSelect when input itself is clicked
            });
            // TODO: conflicts with dropdown click event handler, find work-around
            // TODO: (i.e. when this is activated, clicking an entry in the suggestion list won't update the input's value)
            // NOTE: Using the deferred handling for now so that the blur handler can be canceled from the suggestion
            // NOTE: click handler.
            var blurCanceled = false;
            var blurHandler = function () {
                log('got blur event')
                window.setTimeout(function () {
                    if (!blurCanceled) {
                        self.suggestionSelect.hide();
                    }
                    blurCanceled = false;
                }, 250);
            };
            input.blur(blurHandler);

            input.attr("autocomplete", "off");

            var updateCallback = function (selection) {
                self.updateValue(selection);
                self.input.focus();
            }
            this.suggestionSelect.onchange(updateCallback);
            this.suggestionSelect.onclick(function (selection) {
                log("suggestion clicked");
                blurCanceled = true;
                updateCallback.call(window, selection);
            });
        },

        _remove_wildcards: function (query) {
            if (this.wildcards == null) {
                return query;
            }

            var nwcards = this.wildcards.length;
            for (var i = 0; i < nwcards; i++) {
                query = query.replace(this.wildcards[i], '', 'g');
            }

            return query;
        },

        /**
         * Called when the input's value has changed
         */
        changed: function (ev) {
            if (ev) {
                log('got changed event:', ev);
            }

            var self = this;

            // clear suggestion list
            this.suggestionSelect.clear();

            var curChange = ++this.changeCount;
            window.setTimeout(function () { self.query(curChange); }, this.delay);
        },

        /**
         * Serves as public method to trigger a query for suggestions programatically
         */
        trigger: function () {
            log('autocomplete triggered');
            this.changed();
        },

        /**
         * Called when a control key was pressed, which did not change the input's value
         */
        unchanged: function (ev) {
            log('got unchanged event:', ev)
            if (ev.which == Keys.DOWN || ev.which == Keys.UP
                || ev.which == Keys.PGDN || ev.which == Keys.PGUP) {
                if (!this.suggestionSelect.active()) {
                    log('key down pressed: focusing dropdown');
                    this.showSuggestions();
                }
                else if (ev.which == Keys.DOWN) {
                    this._selectNextSuggestion();
                }
                else if (ev.which == Keys.UP) {
                    this._selectPrevSuggestion();
                }
                else if (ev.which == Keys.PGDN) {
                    this._selectNextSuggestion(this.selectPageStepSize);
                }
                else if (ev.which == Keys.PGUP) {
                    this._selectPrevSuggestion(this.selectPageStepSize);
                }
            }
            else if (ev.which == Keys.HOME || ev.which == Keys.END) {
                if (!this.suggestionSelect.active()) {
                    // let event propagate to input's default handler, when dropdown is not visible
                    return true;
                }

                if (ev.which == Keys.HOME) {
                    this._selectFirstSuggestion();
                }
                else if (ev.which == Keys.END) {
                    this._selectLastSuggestion();
                }

                return false;
            }
            else if (ev.which == Keys.TAB) {
                if (!this.suggestionSelect.active()) {
                    return true;
                }
                if (ev.shiftKey) {
                    this._selectPrevSuggestion();
                }
                else {
                    this._selectNextSuggestion();
                }
                return false;
            }
            else if (ev.which == Keys.ESC) {
                this.suggestionSelect.hide();
            }
            else if (ev.which == Keys.ENTER) {
                var sel = this.suggestionSelect;
                if (sel.active() && sel.selected()) {
                    this._selectCurrSuggestion();
                    sel.hide();
                    return false;
                }
            }

            return true;
        },

        changedDuringDelay: function (curChange) {
            log('cur change', curChange, 'changes', this.changeCount);
            return this.changeCount > curChange;
        },

        query: function(curChange) {
            if (this.changedDuringDelay(curChange)) {
                log('not querying: changed since update request');
                return;
            }

            var value = this.input.attr("value") || "";
            value = $.trim(value);

            if (!this.searchTriggerCheck.call(window, value)) {
                log('search trigger check failed, not triggering search - value:', value);
                return;
            }

            log('querying:', value);
            var self = this;
            var query = value;
            this.searchCallback.call(this.input, query, function (matches) {
                self.updateSuggestions(curChange, matches, query);
            });
        },

        updateSuggestions: function (curChange, suggestions, query) {
            var self = this;

            if (this.changedDuringDelay(curChange)) {
                log('not updating suggestion select: changed since update request');
                return;
            }

            log('updating suggestion select - suggestions: ', suggestions);
            this.suggestionSelect.updateSuggestions(suggestions, function (container, value) {
                self.fillCallback.call(window, container, value, query);
            });
            this.showSuggestions();
        },

        showSuggestions: function () {
            var offset = this.input.offset();
            var height = this.input.outerHeight();
            log('input offset:', offset, 'input height:', height);

            var inputWidth = this.input.width();
            log("input width: ", inputWidth);
            log("input outerWidth: ", this.input.outerWidth());

            var ddOpts = {};

            ddOpts.top = offset.top + height;
            ddOpts.left = offset.left;

            if (this.forceDropDownWidth) {
                log("forcing drop down width:", inputWidth);
                ddOpts.width = inputWidth;
            }
            else if (this.adjustDropDownWidth) {
                log("adjusting drop down width:", inputWidth);
                ddOpts.minWidth = inputWidth;
            }

            this.suggestionSelect.show(ddOpts);
        },

        updateValue: function (value) {
            if (this.textCallback) {
                value = this.textCallback.call(window, value);
            }
            this.input.attr("value", value);
        },

        eagerUpdateValue: function (index) {
            if (this.eagerUpdate) {
                this.updateValue(this.suggestionSelect.select(index).selection());
            }
            else {
                this.suggestionSelect.highlight(index);
            }
        },

        _selectCurrSuggestion: function () {
            this.updateValue(this.suggestionSelect.selection());
        },

        _selectPrevSuggestion: function (step) {
            this.eagerUpdateValue(this.suggestionSelect.prevIndex(step));
        },

        _selectNextSuggestion: function (step) {
            this.eagerUpdateValue(this.suggestionSelect.nextIndex(step));
        },

        _selectFirstSuggestion: function () {
            this.eagerUpdateValue(this.suggestionSelect.firstIndex());
        },

        _selectLastSuggestion: function () {
            this.eagerUpdateValue(this.suggestionSelect.lastIndex());
        }
    })

    $.fn.extend({
        autoComplete: function (options) {
            this.each(function () {
                var input = $(this);
                var inst = new AutoComplete(this, options);
                var data = input.data(AutoComplete.dataName) || {};
                data.instance = inst;
                input.data(AutoComplete.dataName, data);
            })
            return this;
        },

        triggerAutoComplete: function () {
            this.each(function () {
                var input = $(this);
                var data = input.data(AutoComplete.dataName) || {};
                if (!data.instance) {
                    return true;
                }
                data.instance.trigger();
            })
            return this;
        }
    })
    $.fn.autocomplete = $.fn.autoComplete;
    $.fn.triggerAutocomplete = $.fn.triggerAutoComplete;

    /*
     * AutoComplete Example
     */
    /*
    $(document).ready(function () {
        $("#ajax-form").submit(function () {
            log("form submit: value: " + $("#ajax-input").attr("value"));
            return false;
        });
        var search = function (query, resultCallback) {
            var fakeResult = [
                    "Result 1 " + Math.random(),
                    "Result 2 " + Math.random(),
                    "Result 3 " + Math.random(),
                    "Result 4 " + Math.random(),
                    "Result 5 " + Math.random(),
                    "Result 6 " + Math.random(),
                    "Result 7 " + Math.random()
            ];
            window.setTimeout(function () { resultCallback.call(window, fakeResult) }, 50);
        };
        new AutoComplete("#ajax-input", search);
        $("#ajax-input").focus();
    })
    */

})(jQuery)

