/**
 * slider.js -- super simple DHTML slider widget helper.
 */

/**
 * Initializes slider.
 *
 * @param widgetElement
 *   the widget container. The bar and knob must be contained by this element.
 * @param barElement
 *   the bar element, which defines the page position and size of the slider.
 * @param knobElement
 *   the knob element, which defines the moving knob.
 * @param min
 *   lower bounds on underlying data value.
 * @param max
 *   upper bounds on underlying data value.
 * @param granularity
 *   slider granularity, must be >= min and <= max.
 */
function Slider(widgetElement, barElement, knobElement, min, max, granularity)
{
  this._dragging = false;
  this._value = 0;
  this.min = min;
  this.max = max;
  this.granularity = granularity * 1.0;
  this.onChange = null;
  this.onBeginDrag = null;
  this.onEndDrag = null;
  this.barElement = barElement;
  this.knobElement = knobElement;
  this.widgetElement = widgetElement;

  // Event handlers. We always return false is prevent event cascading.
  var slider = this;
  this._mouseMoveListener = function (event)
  {
    slider._updateDrag(event, this);
    event.returnValue = false;
    return false;
  };
  this._mouseDownListener = function (event)
  {
    slider._endDrag(event, this);
    if (event.button == 1 || event.button == 0)
    {
      slider._beginDrag(event, this);
    }
    event.returnValue = false;
    return false;
  };
  this._mouseUpListener = function (event)
  {
    slider._endDrag(event, this);
    event.returnValue = false;
    return false;
  };
  addEventListenerTo(this.widgetElement, "mousedown", this._mouseDownListener);
  addEventListenerTo(document, "mouseup", this._mouseUpListener);
  
  // Initial update
  this._update();
}

/**
 * End slider usage. Unhooks event handlers.
 */
function Slider_detach()
{
  this._endDrag();
  removeEventListenerFrom(this.widgetElement, "mousedown", this._mouseDownListener);
  removeEventListenerFrom(document, "mouseup", this._mouseUpListener);
}

/**
 * Returns current value.
 */
function Slider_getValue(v)
{
  return this._value;
}

/**
 * Sets current value.
 */
function Slider_setValue(v)
{
  this._value = v;
  this._update();
  if (this.onChange)
  {
    this.onChange(this);
  }
}

/**
 * Begins dragging.
 */
function Slider__beginDrag(event, element)
{
  var x = typeof event.layerX != typeof undefined ? event.layerX : event.offsetX;
  if (x >= 0 && x < this.widgetElement.offsetWidth)
  {
    this._dragging = true;
    
    // Disable click handler to avoid cascading of events in Firefox/Moz that
    // enters text selection mode
    removeEventListenerFrom(this.widgetElement, "mousedown", this._mouseDownListener);
    
    // Begin listening to move
    addEventListenerTo(this.widgetElement, "mousemove", this._mouseMoveListener);

    // Initiate dragging
    if (this.onBeginDrag)
    {
      this.onBeginDrag(this);
    }
    
    // Setup first update
    if (x <= this.knobElement.offsetWidth)
    {
      if (typeof event.layerX == typeof undefined)
      {
        // IE is crazy
        x += 2;
      }
      this._dragOffsetX = x;
    }
    else
    {
      this._dragOffsetX = this.knobElement.offsetWidth / 2.0;
    }
    this._updateDrag(event, element);
  }
}

/**
 * Ends dragging.
 */
function Slider__endDrag(event, element)
{
  if (this._dragging)
  {
    this._dragging = false;
    if (this.onEndDrag)
    {
      this.onEndDrag(this);
    }
    
    // Re-add click handler
    addEventListenerTo(this.widgetElement, "mousedown", this._mouseDownListener);
    
    // Disable move handler as we're not dragging anymore
    removeEventListenerFrom(this.widgetElement, "mousemove", this._mouseMoveListener);
  }
}

/**
 * Updates value from knob position.
 */
function Slider__updateDrag(event, element)
{
  if (this._dragging)
  {
    // Determine new value based on click position
    function getAbsoluteLeft(e)
    {
      var x = e.offsetLeft;
      if (e.offsetParent)
      {
        x += getAbsoluteLeft(e.offsetParent);
      }
      return x;
    }
    var scrollX = typeof window.scrollX != typeof undefined ? 
      window.scrollX : document.body.scrollLeft;
    var relX = getAbsoluteLeft(this.barElement) - scrollX;
    var left = (event.clientX - relX) -
      this._dragOffsetX + (this.knobElement.offsetWidth / 2.0) - 1;
    var newValue =
      (left / (1.0 * this.barElement.offsetWidth)) * this.max;
    newValue = this._roundValue(newValue);
    this.setValue(newValue);
  }
}

/**
 * Normalizes value.
 */
function Slider__roundValue(v)
{
  // Bind and round value according to granularity
  var grains = (this.max - this.min) / this.granularity;
  var step = (this.max - this.min) / grains;
  v = this.min + Math.round((v - this.min) / this.granularity) * step;
  if (v < this.min)
  {
    v = this.min;
  }
  else if (v > this.max)
  {
    v = this.max;
  }
  return v;
}

/**
 * Updates knob position from value.
 */
function Slider__update()
{
  var newLeft =
    ((this._value - this.min) / (this.max - this.min)) *
    this.barElement.offsetWidth - this.knobElement.offsetWidth / 2.0 - 1;
  this.knobElement.style.left = newLeft + "px";
}

Slider.prototype.detach = Slider_detach;
Slider.prototype.setValue = Slider_setValue;
Slider.prototype.getValue = Slider_getValue;

Slider.prototype._beginDrag = Slider__beginDrag;
Slider.prototype._endDrag = Slider__endDrag;
Slider.prototype._updateDrag = Slider__updateDrag;
Slider.prototype._roundValue = Slider__roundValue;
Slider.prototype._update = Slider__update;
