
/**
 * Cursor class.
 */
function Cursor(e, set)
{
  // Instance variables.
  this.texpX = null;
  this.tempY = null;
  
  // Class methods.
  this._cursor = _cursor;
  this.getPosition = getPosition;
  this.moveElement = moveElement;
  this.movePosition = movePosition;
  
  this._cursor(e, set);
  
  /**
   * If set is true assigns instance variables tempX and tempY with current
   * cursor position.
   *
   * @param object [MouseEvent], boolean
   */
  function _cursor(e, set)
  {
    if(set)
    {
      var position = this.getPosition(e);
      
      this.tempX = position.x;
      this.tempY = position.y;
    }
  }
  
  function getPosition(e)
  {
    var x = null;
    var y = null;
    
    if(window.event)
    {
      x = window.event.clientX + (document.documentElement.scrollTop ? 
          document.documentElement.scrollTop : document.body.scrollTop);
      y = window.event.clientY + (document.documentElement.scrollTop ?
          document.documentElement.scrollTop : document.body.scrollTop);
    }
    else if(e.pageX && e.pageY)
    {
      x = e.pageX;
      y = e.pageY;
    }
    return {"x": x, "y": y};
  }
  
  function moveElement(e, element, axis)
  {
    var position = this.getPosition(e);
    var offsetX = 0;
    var offsetY = 0;
    
    if(axis.x) {
      offsetX = this.tempX - position.x;
    }    
    if(axis.y) {
      offsetY = this.tempY - position.y;
    }
    element.style.left = element.offsetLeft - offsetX;
    element.style.top = element.offsetTop - offsetY;
    
    this.tempY = position.y;
    this.tempX = position.x;
  }
  
  function movePosition(e, element, axis)
  {
    var position = this.getPosition(e);
    var offsetX = 0;
    var offsetY = 0;
    
    if(axis.x) {
      offsetX = this.tempX - position.x;
    }    
    if(axis.y) {
      offsetY = this.tempY - position.y;
    }
    element.x = element.x - offsetX;
    element.y = element.y - offsetY;
    
    this.tempY = position.y;
    this.tempX = position.x;
  }
}

function Key()
{
  // Class methods.
  this.ascii = ascii;
  
  function ascii(e)
  {
    if(window.event)
    {
      return window.event.keyCode;
    }
    else if(e.which)
    {
      return e.which;
    }
    else
    {
      return null;
    }
  }
}

/**
 * Autocomplete class is a wrapper for the TextArea and Scrollbar classes.
 * It provides an interface for autocomplete implementation within a html code.
 * Requires Key class to be included.
 *
 * @author David Stefan
 * @version 1.0
 */
function Autocomplete(inputId,containerId,textAreaId,scrollbarId,hiddenId,scriptUrl)
{
  var _this = this;
  
  // Instance variables.
  this.inputId = null;
  this.containerId = null;
  this.scrollbarId = null;
  this.textAreaId = null;
  this.hiddenId = null;
  this.scriptUrl = null;
  
  this.inputElement = null;
  this.hiddenElement = null;
  this.textArea = null;
  this.scrollbar = null;
  
  this.key = null;
  this.ajax = null;
  
  // Class methods.
  this._autocomplete = _autocomplete;  
  this.inputKeyUp = inputKeyUp;
  this.inputKeyDown = inputKeyDown;
  this.inputEnter = inputEnter;
  this.inputUpArrow = inputUpArrow;
  this.inputDownArrow = inputDownArrow;
  this.ajaxComplete = ajaxComplete;
  
  // Constructor call.
  this._autocomplete(inputId,containerId,scrollbarId,textAreaId,hiddenId,scriptUrl);
  
  /**
   * Autocomplete constructor takes components' ids as its arguments and
   * registers represented elements with instance variables. Also creates Key
   * and Ajax instances and assigns handler functions to the input element
   * mouse events.
   *
   * @param objectId, objectId, objectId, objectId
   */
  function _autocomplete(inputId,containerId,scrollbarId,textAreaId,hiddenId,scriptUrl)
  {
    this.inputId = inputId;
    this.containerId = containerId;
    this.scrollbarId = scrollbarId;
    this.textAreaId = textAreaId;
    this.hiddenId = hiddenId;
    this.scriptUrl = scriptUrl;
    
    this.inputElement = document.getElementById(this.inputId);
    this.hiddenElement = document.getElementById(this.hiddenId);
    
    this.key = new Key();
    this.ajax = new Ajax();
    
    document.getElementById(this.inputId).onkeyup = inputKeyUp;
    document.getElementById(this.inputId).onkeydown = inputKeyDown;
    this.ajax.oncomplete = ajaxComplete;

    this.onshow = null;
  }
  
  function inputKeyUp(e)
  {
    var key = _this.key.ascii(e);
    if(key != 13 && key != 38 && key != 40)
    {
      var str = _this.inputElement.value;
      if(str.length > 2)
      {
        _this.ajax.update("a", _this.scriptUrl, "str="+str, {"target": _this.containerId});
      }
    }
  }
  
  function inputKeyDown(e)
  {
    switch(_this.key.ascii(e))
    {
    	case 9: {
    		_this.textArea.hide();
    		_this.scrollbar.hide();
    		break;
    	}
      case 13: {
        _this.inputEnter()
        break;
      }
      case 38: {
        _this.inputUpArrow();
        break;
      }
      case 40: {
        _this.inputDownArrow();
        break;
      }
    }
  }
  
  function inputEnter()
  {
    this.hiddenElement.value =
    	this.textArea.getSelectedItem().attributes.getNamedItem("value").value;
    this.inputElement.value = 
        this.textArea.getSelectedItem().innerHTML;
    this.textArea.hide();
    this.scrollbar.hide();
  }
  
  function inputUpArrow()
  {
    if(this.textArea)
    {
      this.textArea.selectPrevious();
    }
  }
  
  function inputDownArrow()
  {
    if(this.textArea)
    {
      this.textArea.selectNext();
    }
  }
  
  /**
   * Creates new instances of TextArea and Scrollbar and registers handler
   * functions with its events. 
   */
  function ajaxComplete(xmlHttp, optArray)
  {
    if(xmlHttp.responseText)
    {
      document.getElementById(_this.containerId).innerHTML = xmlHttp.responseText;
          
      _this.textArea = new TextArea(_this.textAreaId, _this.containerId);
      _this.scrollbar = new Scrollbar(_this.scrollbarId, document.getElementById(_this.containerId));
      
      if(_this.textArea.items.length > 0)
      {
        _this.textArea.onitemdblclick = function(item)
        {
          _this.hiddenElement.value = _this.textArea.getSelectedItem().attributes.getNamedItem("value").value;
          _this.inputElement.value = item.innerHTML;
          _this.textArea.hide();
          _this.scrollbar.hide();
        }
        
        if(_this.textArea.relativeHeight > 0)
        {        
          _this.textArea.onshift = function(offset)
          {
            var factor = _this.scrollbar.getFactor(this.relativeHeight);
            _this.scrollbar.setOffset(offset * factor);
          }
          
          _this.scrollbar.onslidermove = function(offset)
          {
            var factor = _this.textArea.getFactor(this.relativeHeight);
            _this.textArea.setOffset(offset * factor);
          }
          _this.scrollbar.show();
        }
        _this.textArea.show();
      }
    }
    _this.onshow ? _this.onshow() : null;
  }
}

/**
 * TextArea class.
 */
function TextArea(textAreaId, containerId)
{
  var _this = this;
  
  // Instance variables.
  this.textArea = null;
  this.container = null;
  this.items = [];
  
  // TextArea properties.
  this.maxTop = null;
  this.maxBottom = null;
  this.relativeHeight = null;
  this.selectedItem = null;
  this.selectedColor = "#F37321";
  
  // Class methods.
  this._textArea = _textArea;
  this.setComponent = setComponent;
  this.setEvents = setEvents;
  
  // Methods to handle items.
  this.selectNext = selectNext;
  this.selectPrevious = selectPrevious;
  this.itemMouseDown = itemMouseDown;
  this.itemDblClick = itemDblClick;
  this.selectItem = selectItem;
  this.deselectItem = deselectItem;
  this.getSelectedItem = getSelectedItem;
  
  // Methods to handle textArea.
  this.shift = shift;
  this.setOffset = setOffset;
  this.getFactor = getFactor;
  this.show = show;
  this.hide = hide;
  
  this.onshift = null;
  this.onitemmousedown = null;
  this.onitemdblclick = null;
  
  // Constructor call.
  this._textArea(textAreaId, containerId);
  
  function _textArea(textAreaId, containerId)
  {
    this.container = document.getElementById(containerId);
    var stack = [document.getElementById(textAreaId)];
    
    if(document.getElementById(textAreaId))
    {
    while(stack.length > 0)
    {
      var component = stack.shift();
      var childNodes = component.childNodes;
      
      this.setComponent(component);
      
      for(i = 0; i < childNodes.length; i++)
      {
        stack.push(childNodes[i]);
      }
    }
    this.setEvents();
    this.offsetError = this.textArea.offsetTop;
    this.relativeHeight =
      this.container.offsetHeight < this.textArea.offsetHeight ?
      this.textArea.offsetHeight - this.container.offsetHeight + 2 : 0;
    }
  }
  
  function setComponent(component)
  {
    switch(component.className)
    {
      case "textArea": {
        this.textArea = component;
        break;
      }
      case "item": {
        component.style.width = this.container.offsetWidth;
        this.items.push(component);
        break;
      }
    }
  }
  
  function setEvents()
  {
    for(i = 0; i < this.items.length; i++)
    {
      this.items[i].onmousedown = itemMouseDown;
      this.items[i].ondblclick = itemDblClick; 
    }
  }
  
  function selectNext()
  {
    if(this.selectedItem == null)
    {
      this.selectedItem = 0;
    }
    else
    {
      this.deselectItem(this.selectedItem);
      this.selectedItem = (this.selectedItem + 1) % this.items.length;
    }
    this.selectItem(this.selectedItem);
    this.shift();
  }
  
  function selectPrevious()
  {
    if(this.selectedItem == null)
    {
      this.selectedItem = this.items.length - 1;
    }
    else
    {
      this.deselectItem(this.selectedItem);
      this.selectedItem =
        (this.selectedItem + this.items.length - 1) % this.items.length;
    }
    this.selectItem(this.selectedItem);
    this.shift();
  }
  
  function getSelectedItem()
  {
    return this.items[this.selectedItem];
  }
  
  function itemMouseDown()
  {
    _this.selectedItem != null ? _this.deselectItem(_this.selectedItem) : null;
    _this.selectedItem = parseInt(this.attributes.getNamedItem("index").value);
    _this.selectItem(_this.selectedItem);
    _this.shift();
    
    // Triggers onitemmousedown event.
    _this.onitemmousedown ?
      _this.onitemmousedown(_this.items[_this.selectedItem]) : null;
  }
  
  function itemDblClick()
  {
    // Triggers onitemdblclick event.
    _this.onitemdblclick ?
      _this.onitemdblclick(_this.items[_this.selectedItem]) : null;
  }
  
  function shift()
  {
    var item = this.items[this.selectedItem];
    var itemRelativeOffset = item.offsetTop + this.textArea.offsetTop;
    
    if(itemRelativeOffset < 0)
    {
      var offset = itemRelativeOffset;
      this.textArea.style.top = this.textArea.offsetTop - offset;
      this.onshift ?
        this.onshift(this.textArea.offsetTop - this.offsetError) : null;
    }
    else if(itemRelativeOffset + item.offsetHeight > this.container.offsetHeight)
    {
      var offset = itemRelativeOffset + item.offsetHeight - 
        this.container.offsetHeight + 2; // The 2 is for the container border.
      this.textArea.style.top = this.textArea.offsetTop - offset;
      this.onshift ?
        this.onshift(this.textArea.offsetTop - this.offsetError) : null;
    }
  }
  
  function show()
  {
    this.container.style.visibility = "visible";
  }
  
  function hide()
  {
    this.container.style.visibility = "hidden";
  }
  
  function getFactor(height)
  {
    return -(this.relativeHeight / height);
  }
  
  function setOffset(offset)
  {
    this.textArea.style.top = offset;
  }
  
  function selectItem(i)
  {
    this.items[i].style.background = this.selectedColor;
  }
  
  function deselectItem(i)
  {
    this.items[i].style.background = "none";
  }
}

/**
 * Scrollbar class.
 */
function Scrollbar(scrollbarId, activeArea)
{
  var _this = this;
  
  // Instance variables.
  this.scrollbar = null;
  this.upArrow = null;
  this.downArrow = null;
  this.slider = null;
  this.activeArea = null;
  this.virtualSlider = null;
  
  this.cursor = null;
  this.scroll = false;
  this.frozen = false;
  
  // Scrollbar properties.
  this.relativeHeight = null;
  this.sliderRelativeTop = null;
  this.sliderRelativeBottom = null;
  
  // Class methods.
  this._scrollbar = _scrollbar;
  this.setComponent = setComponent;
  this.setActiveArea = setActiveArea;
  this.setEvents = setEvents;
  this.freeze = freeze;
  this.defrost = defrost;
  this.setOffset = setOffset;
  this.getFactor = getFactor;
  
  this.show = show;
  this.hide = hide;
  
  this.onslidermousedown = null;
  this.onareamouseup = null;
  this.onslidermove = null;
  
  // Constructor call.
  this._scrollbar(scrollbarId, activeArea);
  
  /**
   * Scrollbar constructor implements DFS to search DOM tree with scrollbar
   * element at its root. Assigns element to appropriate instance variable
   * whenever such element matches scrollbar's class name criteria.
   *
   * @param objectId, object [HTMLDivElement]
   */
  function _scrollbar(scrollbarId, activeArea)
  {
    var stack = [document.getElementById(scrollbarId)];
    
    while(stack.length > 0)
    {
      var component = stack.shift();
      var childNodes = component.childNodes;
      
      this.setComponent(component);
      
      for(i = 0; i < childNodes.length; i++)
      {
        stack.push(childNodes[i]);
      }
    }    
    this.relativeHeight = 
      this.scrollbar.offsetHeight - this.upArrow.offsetHeight - 
      this.downArrow.offsetHeight - this.slider.offsetHeight;
    
    this.sliderRelativeTop = this.upArrow.offsetHeight;
    this.sliderRelativeBottom =
      this.downArrow.offsetTop - this.slider.offsetHeight;
    
    this.setActiveArea(activeArea);
    this.setEvents();
    
    this.virtualSlider = {"x": null, "y": null};
  }
  
  /**
   * Registers handler functions with components' mouse events.
   */
  function setEvents()
  {
    this.slider.onmousedown = sliderMouseDown;
    this.activeArea.onmouseup = areaMouseUp;
    this.activeArea.onmousemove = sliderMove;
  }
  
  /**
   * This function is called from the context of class constructor. If given, 
   * activeArea is set to its argument. Otherwise activeArea is set to the area
   * of scrollbar itself.
   *
   * @param (object [HTMLElement])
   */
  function setActiveArea(area)
  {
    if(area)
    {
      this.activeArea = area;
    }
    else
    {
      this.activeArea = this.scrollbar;
    }
  }
  
  function sliderMouseDown(e)
  {
    _this.scroll = true;
    _this.cursor = new Cursor(e, true);
    _this.onslidermousedown ? _this.onslidermousedown(e) : null;
  }
  
  function areaMouseUp(e)
  {
    _this.scroll = false;
    _this.onareamouseup ? _this.onareamouseup(e) : null;
    _this.frozen = false;
  }
    
  function sliderMove(e)
  {    
    if(_this.scroll)
    {
      if(_this.frozen)
      {
        _this.cursor.movePosition(e, _this.virtualSlider, {"y": true});
        _this.defrost();
      }
      else
      {
        _this.cursor.moveElement(e, _this.slider, {"y": true});  
        _this.freeze();
        _this.onslidermove ? _this.onslidermove(_this.slider.offsetTop - _this.sliderRelativeTop) : null;
      }
    }
  }
  
  function getFactor(height)
  {
    return -(this.relativeHeight / height);
  }
  
  function setOffset(offset)
  {
    this.slider.style.top = this.sliderRelativeTop + offset;
  }
  
  function setComponent(component)
  {
    switch(component.className)
    {
      case "scrollbar": {
        this.scrollbar = component;
        break;
      }
      case "upArrow": {
        this.upArrow = component;
        break;
      }
      case "downArrow": {
        this.downArrow = component;
        break;
      }
      case "slider": {
        this.slider = component;
        break;
      }
    }
  }
  
  function show()
  {
    this.scrollbar.style.visibility = "visible";
  }
  
  function hide()
  {
    this.scrollbar.style.visibility = "hidden";
  }
  
  function freeze()
  {
    if(this.slider.offsetTop <= this.sliderRelativeTop)
    { 
      var offsetError = -(this.sliderRelativeTop - this.slider.offsetTop);
      this.slider.style.top = this.sliderRelativeTop;
      this.frozen = true;
    }
    else if(this.slider.offsetTop >= this.sliderRelativeBottom)
    {
      var offsetError = this.slider.offsetTop - this.sliderRelativeBottom;
      this.slider.style.top = this.sliderRelativeBottom;
      this.frozen = true;
    }
    
    if(this.frozen)
    {  
      this.virtualSlider.y = this.slider.offsetTop + offsetError;
    }
  }
  
  function defrost()
  {
    if(this.virtualSlider.y >= this.sliderRelativeTop &&
       this.virtualSlider.y <= this.sliderRelativeBottom)
    {
      this.frozen = false;
      this.slider.style.top = this.virtualSlider.y;
    }
  }
}
