/**
 * Copyright 2010 Internet Scout Project
 * http://scout.wisc.edu/
 */

/**
 * Construct an AJAX dropdown object.
 */
function AjaxDropdown() {
    this.$Container = $("<div class=\"dd_container\"><div class=\"dd_message\">"+
        "</div><div class=\"dd_results\"></div></div>");
    this.$Message = $(".dd_message", this.$Container);
    this.$Results = $(".dd_results", this.$Container);
    this.$Container.appendTo(document.body);
    this.Animating = false;
    this.Cache = {};
}

/**
 * Hook AJAX dropdown functionality the given jQuery elements.
 * @param $group jQuery object naming the elements to hook to
 * @param options various options
 */
AjaxDropdown.prototype.hookTo = function($group, options) {
    var usekeydown = (navigator.userAgent.search(/webkit/i) >= 0
      || navigator.userAgent.search(/msie/i) >= 0);
    var controlDown = false;
    options = $.extend({
      "url": "index.php?P=AJAXDropdown&Version=2",
      "parameterFn": function($elem){
        return {"input": $elem.val()};
      },
      "selectFn": function(){}
    }, options);

    (function($group, $this, options, usekeydown){
        $group.focus(function(){
            var $elem = $(this);
            $elem.select();
            $elem.bind((usekeydown ? "keydown" : "keypress"), key);
            $elem.bind("keyup", keyup);
            $this.attachTo($elem);
        });

        $group.blur(function(){
            var $elem = $(this);
            $elem.unbind((usekeydown ? "keydown" : "keypress"), key);
            $elem.unbind("keyup", keyup);
            $this.detachFrom($elem);
            $this.reset();
        });

        function keyup(e) {
          if (e.keyCode == 17 && controlDown) {
            controlDown = false;
          }
        }

        $group.onvaluechange(function($elem, prev, curr){
            $elem.removeClass("dd_validvalue");

            if ($.trim(curr).length < AjaxDropdown.MINLEN) {
                $this.reset();
                $this.$Message.html("Enter at least "+AjaxDropdown.MINLENLANG+
                    " characters to begin searching.");
            } else if ("undefined" != typeof $this.Cache[curr]) {
                $this.reset();
                populate($this.Cache[curr], $elem);
            }
        }, {"checkInterval": 20});

        $group.onvaluechange(function($elem, prev, curr){
            if ($.trim(curr).length < AjaxDropdown.MINLEN) {  return;  }

            if ("undefined" != typeof $this.Cache[curr]) {
                return;
            }

            $.ajax({
                "url": options.url,
                "dataType": "json",
                "data": options.parameterFn($elem),
                "success": function(resp){ handle(resp, $elem, curr); }
            });
        }, {"checkInterval": 600});

        function key(e) {
             if (e.keyCode == 17) { // control
                controlDown = true;
                return false;
             } else if (e.keyCode == 38 || e.keyCode == 40
                || (controlDown && (e.keyCode == 80 || e.keyCode == 78))) { // up/down
                if (!$this.$Results.children().length) {
                    return;
                }

                // if there is no selected result, pick the first one
                if ($this.$SelectedResult == null) {
                    $this.$SelectedResult = $("div:first-child", $this.$Results);
                    $this.$SelectedResult.addClass("dd_selected");
                } else {
                    // up arrow gets prev, down arrow gets next
                    var $select = (e.keyCode == 38 || e.keyCode == 80)
                        ? $this.$SelectedResult.prev() : $this.$SelectedResult.next();
                    if ($select.length) {
                        $this.$SelectedResult.removeClass("dd_selected");
                        $this.$SelectedResult = $select;
                        $this.$SelectedResult.addClass("dd_selected");
                    }
                }

                // scroll to show results if necessary
                var $s = $this.$SelectedResult;
                var pos = $s.offset();
                if (!$this.Animating && pos.top - $(window).scrollTop() < 65) {
                    // block animations
                    $this.Animating = true;

                    // show previous resources
                    $.scrollTo(pos.top - 110, {
                        "axis": "y",
                        "duration": 120,
                        "easing": "linear",
                        "onAfter": function(){
                            // unblock animations
                            $this.Animating = false;
                        }
                    });
                } else if (!$this.Animating && $(window).scrollTop()
                    + $(window).height() - pos.top - $s.outerHeight(true) < 65)
                {
                    // block animations
                    $this.Animating = true;

                    // show upcoming resources
                    $.scrollTo(110 - $(window).height() + pos.top +
                            $s.outerHeight(true), {
                        "axis": "y",
                        "duration": 150,
                        "easing": "linear",
                        "onAfter": function(){
                            // unblock animations
                            $this.Animating = false;
                        }
                    });
                }

                return false;
            } else if (e.keyCode == 13) { // enter
                // return false to prevent form submission
                if ($this.$SelectedResult != null) {
                    // change the tokens back to their original values
                    var html = $this.$SelectedResult.attr("fieldvalue");
                    html = html.replace(/\[DD_LESS_THAN\]/gi, "<");
                    html = html.replace(/\[DD_GREATER_THAN\]/gi, ">");

                    select($(this), html);
                    return false;
                } else if ($.trim($this.$Attached.val()) != "") {
                    return false;
                }
            } else if (e.keyCode == 27) { // escape
                $(this).blur();
            }
        }

        function select($elem, text) {
            // scroll back if necessary
            var pos = $elem.offset();
            if (pos.top-$(window).scrollTop() < 65
                || $(window).scrollTop()+$(window).height()-pos.top < 65) {
                $.scrollTo($elem, {
                    "duration": 200,
                    "easing": "swing",
                    "offset": {"top": -110, "left": -25}
                });
            }

            // $elem.val() must come before $elem.blur()!! (IE issues otherwise)
            $elem.val(text.replace(/&amp;/gi, "&"));
            $elem.blur();
            $elem.addClass("dd_validvalue");
            options.selectFn($this, $elem);
        }

        function handle(response, $elem, val) {
            // a stale response, don't update
            if ($this.$Attached !== null
                && $this.$Attached.get(0) != $elem.get(0)) {
                return;
            }

            $this.reset();

            if (response.status.state != "OK") {
                alert(response.status.message);
                return;
            }

            if (response.data.error) {
                $this.$Message.html(response.data.errorMessage);
                return;
            }

            populate(response.data.results, $elem);
            $this.Cache[val] = response.data.results;
        }

        function populate(results, $elem) {
            var html = "";
            var offset = 1;
            for (var i in results) {
                var enc = results[i].replace(/\</gi, "&lt;").replace(/\>/gi, "&gt;");
                var num = (offset++ % 2 == 0) ? "dd_evenresult" : "dd_oddresult";

                // need to tokenize < and > so that existing &lt; and &gt;
                // aren't decoded later on if they exist in the controlled
                // name
                var value = results[i].replace(/\</gi, "[DD_LESS_THAN]");
                value = value.replace(/\>/gi, "[DD_GREATER_THAN]");

                html += "<div class=\"dd_result "+num+"\" fieldvalue=\""+
                    value+"\">"+enc+"</div>";
            }
            $this.$Results.html(html);
            $children = $this.$Results.children();
            $children.mouseover(function(){  $(this).addClass("dd_hovered");  });
            $children.mouseout(function(){  $(this).removeClass("dd_hovered");  });
            $children.mousedown(function(){
                // change the tokens back to their original values
                var html = $(this).attr("fieldvalue");
                html = html.replace(/\[DD_LESS_THAN\]/gi, "<");
                html = html.replace(/\[DD_GREATER_THAN\]/gi, ">");

                select($elem, html);
            });
        }
    })($group, this, options, usekeydown);
};

/**
 * Attach the dropdown to the given jQuery element.
 * @param $elem jQuery object to attach to
 */
AjaxDropdown.prototype.attachTo = function($elem) {
    this.$Attached = $elem;

    // reposition/resize the dropdown
    this.updatePosition();

    // add an appropriate message
    if ($.trim($elem.val()).length < AjaxDropdown.MINLEN) {
        this.$Message.html("Enter at least "+AjaxDropdown.MINLENLANG+
        " characters to begin searching.");
    } else {
        this.$Message.html("Enter text to begin searching.");
    }

    // finally display the dropdown
    this.$Container.show();
};

/**
 * Update the size and position of the drop down if possible.
 */
AjaxDropdown.prototype.updatePosition = function() {
    if (!this.$Attached) {
        return;
    }

    // reposition/resize the dropdown
    var pos = this.$Attached.offset();
    var bwidth = parseInt(this.$Container.css("borderLeftWidth"), 10) +
        parseInt(this.$Container.css("borderRightWidth"), 10);
    this.$Container.css({
        "top": (pos.top + this.$Attached.outerHeight())+"px",
        "left": pos.left+"px",
        "width": (this.$Attached.outerWidth()-bwidth)+"px"
    });
};

/**
 * Detach the dropdown from the given jQuery element.
 * @param $elem jQuery object to detach from (not strictly necessary)
 */
AjaxDropdown.prototype.detachFrom = function($elem) {
    this.$Attached = null;
    this.$Container.hide();
    this.reset();
};

/**
 * Reset the dropdown, i.e., clear the message and results layers.
 */
AjaxDropdown.prototype.reset = function() {
    this.$Message.html("");
    this.$Results.html("");
    this.$SelectedResult = null;
};

/**
 * Clear any cached values.
 */
AjaxDropdown.prototype.clearCache = function() {
    this.Cache = {};
};

/**
 * @var AjaxDropdown.MINLEN minimum length of a string for a query to be executed
 * @var AjaxDropdown.MINLENLANG the minimum length in sentence form
 * @var Cache results cache
 * @var $Attached the jQuery object currently attached to
 * @var $Container the dropdown container
 * @var $Message the message div inside the container
 * @var $Results the results div inside the container
 * @var $SelectedResult the currently selected/highlighted result
 */
AjaxDropdown.MINLEN = 3;
AjaxDropdown.MINLENLANG = "three";
AjaxDropdown.prototype.Animating = null;
AjaxDropdown.prototype.Cache = null;
AjaxDropdown.prototype.$Attached = null;
AjaxDropdown.prototype.$Container = null;
AjaxDropdown.prototype.$Message = null;
AjaxDropdown.prototype.$Results = null;
AjaxDropdown.prototype.$SelectedResult = null;
