/* Definitions for sliders, weights, and ajax stuffs
 * requires prototype.js, util.js and slider.js
*/

var Demo = Prototype.emptyFunction;
Demo._currentUrl = window.location.href;

//---------------------------------------------------------------------
// these methods are registered by Behaviour, and are callbacks for
// javascript-links

Demo.showSearchForm = function(event) {
  $('SearchFreeTextInput').value =  $("DrillDownFreeTextInput").value;
  Element.hide('DrillDownFormContainer');
  Element.show('SearchFormContainer');
  Element.removeClassName('DrillDownTab', 'current');
  Element.addClassName('SearchTab', 'current');
  Demo.Slider.setup();
};

Demo.showDrillDown = function(event) {
  $('DrillDownFreeTextInput').value = $("SearchFreeTextInput").value;
  Element.show('DrillDownFormContainer');
  Element.hide('SearchFormContainer');
  Element.addClassName('DrillDownTab', 'current');
  Element.removeClassName('SearchTab', 'current');
  Demo.Slider.setup();
};

//---------------------------------------------------------------------

// a port of the ruby Observable class
// intended to be used as a mixin
Demo.Observable = {
  addObserver: function(observer) {
    if (!this.__observers) {
      this.__observers = new Array();
    }
    if (!isFunction(observer.update)) {
      throw("observer must have an .update method!");
    }
    Log.debug('added observer: ' + observer + ' to ' + this);
    this.__observers.push(observer);
  },
  
  deleteObserver: function(observer) {
    if (this.hasOwnProperty('__observers')) {
      var iterator = function(obs, idx) { return (obs == observer); }
      this.__observers = this.__observers.reject(iterator);
    }
  },

  deleteObservers: function(observer) {
    if (this.hasOwnProperty('__observers')) {
      delete this.__observers;
      this.__observers = new Array();
    }
  },

  countObservers: function() {
    return (this.__observers) ? this.__observers.length() : 0;
  },

  changed: function(state) {
    this.__observer_state = state;
  },

  hasChanged: function() {
    return (this.hasOwnProperty('__observer_state') && this.__observer_state);
  },

  notifyObservers: function(args) {
    if (this.hasOwnProperty('__observer_state') && this.__observer_state) {
      if (this.hasOwnProperty('__observers')) {
        this.__observers.each(function(o, idx) {
          o.update(args);
        });
      }
      this.__observer_state = false;
    }
  }
};

// an observable, dimension and value properties
Demo.Weight = Class.create();
Demo.Weight.prototype = Object.extend(Demo.Observable, {
  initialize: function(dimension, weight) {
    this.dimension = dimension;
    this._initial = this._weight = weight;
  },

  /* returns true if this weight was changed from its initial value
  */
  altered: function() {
    return (this._initial != this._weight);
  },
  
  setValue: function(value) {
    Log.debug("weight for '" + this.dimension + "' set to: " + value);
    this._weight = value;
    this.changed(true);
    this.notifyObservers(this);
  },

  getValue: function() {
    return this._weight;
  }
});


Demo._weights = $H();

Demo.weights = {
  _each: function(iterator) {
    this._weights.each(iterator);
  },

  addWeight: function(dimension, value) {
    var w = new Demo.Weight(dimension, value); 
    Demo._weights[dimension] = w;
    return w;
  },

  addObserver: function(dimension, observer) {
    Demo._weights[dimension].addObserver(observer);
  },

  getWeight: function(dimension) {
    return Demo._weights[dimension];
  },
  
  // returns an array of all weights that have been changed
  // from their original values
  alteredWeights: function() {
    return Demo._weights.values().select(function(weight) { return weight.altered(); });
  }
};

Object.extend(Demo.weights, Enumerable);


Demo._currentTimeoutId = null;

// monitors changes to a weight and performs an ajax update when 
// necessary
Demo.UpdateListener = Class.create();
Demo.UpdateListener.prototype = {
  initialize: function(dimension) {
    this.dimension = dimension
  },

  // --- status callbacks for ajax request ---------------

  onLoading: function(transport, json) {
    Log.debug(this.dimension + ": onLoading called");
    $('SearchStatusText').innerHTML = "Loading results...";
    $('SearchStatusBox').style.display = 'block';
  },

  onLoaded: function(transport, json) {
    Log.debug(this.dimension + ": onLoaded called");
  },

  onInteractive: function(transport, json) {
    Log.debug(this.dimension + ": onInteractive called");
  },

  onComplete: function(transport, json) {
    Log.debug(this.dimension + ": onComplete called");
    Element.hide('SearchStatusBox');
  },

  //------------------------------------------------------

  // breaks the current url into "chunks" (splits on '&')
  chunkCurrentUrl: function() {
    var chunks = window.location.href.split("&");

    var newChunks = chunks.select(function(val,idx) {
      return (isString(val) && (!isEmpty(val)));
    });

    Log.dump(newChunks);

    return newChunks;
  },

  // create a new url based on the current weights
  newSliderUrl: function() {
    var chunks = this.chunkCurrentUrl();

    Demo._weights.each(function(kv) {
      var dimensionId = kv.key;
      var weight = kv.value;

      if (!isNull(weight) && !isUndefined(weight)) {
        chunks.push(escape(dimensionId + "[weight]") + "=" + escape(weight.getValue()));
      }
    });

    return chunks.join('&');
  },

  // performs an Ajax query based on the current value of the sliders
  // and updates the results table with the result
  doAjaxUpdate: function() {
    // if we got here, _currentTimeoutId has been fulfilled
    Demo.Slider._currentTimeoutId = null;

    var url = this.newSliderUrl();
    url = url.replace(/\/search\/results/, '/search/embed');
    if (url != Demo._currentUrl) {
      Demo._currentUrl = url;

      Log.debug("new url: " + url);

      if (Demo._ajaxRequest) {
        Demo._ajaxRequest.transport.abort();
        Demo.ajacRequest = null;
      }

      /* here we set up the 'options' object to pass to the Ajax.Updater ctor
      * we bind a number of methods on this object, so that we're called from
      * a sane context.
      */
      var options = { method: 'get' };
      var optionMethods = ['onLoading', 'onLoaded', 'onInteractive', 'onComplete'];
      var self = this;
      
      optionMethods.each(function(methName) {
        options[methName] = self[methName].bind(self);
      });

      // the ajax request
      Demo._ajaxRequest = new Ajax.Updater(
        { success: 'SearchResultsTableContainer' }, url, options
      );
      return true;
    }
  },

  // schedules an Ajax request to update the results table to occur after
  // a short delay. 
  scheduleAjax: function() {
    Demo.UpdateListener.cancelAjax();

    var self = this;
    Demo.Slider._currentTimeoutId = setTimeout(self.doAjaxUpdate.bind(self), 500);
  },

  update: function() {
    this.scheduleAjax();
  }
};

Object.extend(Demo.UpdateListener, {
  // cancels an ajax update if one is scheduled because of this slider
  // returns true if an ajax update was cancelled, otherwise false
  cancelAjax: function() {
    var rval = true;
    if (Demo._currentTimeoutId) {
      window.clearTimeout(Demo._currentTimeoutId);
      Demo._currentTimeoutId = null;
      Log.debug("cancelled timeout before update");
      rval = true;
    }
    if (Demo._ajaxRequest) {
      Demo._ajaxRequest.transport.abort();
      Demo._ajaxRequest = null;
      Log.debug("cancelled update-in-progress");
      rval = true;
    }
    Element.hide('SearchStatusBox');
    return rval;
  }
});


Demo.sliders = $H();
Object.extend(Demo.sliders, {
  updateAll: function() {
    this.each(function(pair) {
      /* make sure slider has rendered itself 
      */
      pair.value.updateSliderWidget();
    });
  },
  disableAll: function() {
    this.each(function(pair) {
      pair.value.slider.setDisabled();
    });
  },
  enableAll: function() {
    this.each(function(pair) {
      pair.value.slider.setEnabled();
    });
  }
});

Demo.Slider = Class.create();
Demo.Slider.descriptions = [
  "Not important",
  "Slightly important",
  "Important",
  "Very important",
  "Most important"
];

Demo.Slider.prototype = {
   initialize: function(widget_id, handle_id, track_id, help_id, dimension, weight) {
    this.suspended = true;
    this.dimension = dimension;
    this.widget = $(widget_id);
    this.tooltip = $(help_id);
    this.weight = weight;

    // for enclosing a reference to the current object below
    var self = this;

    this.slider = new Control.Slider(
      handle_id,
      track_id,
      { sliderValue: this.weight.getValue(),
        onSlide: self.onSlide.bind(self),
        onChange: self.onChange.bind(self) }
    );
   
    this.suspended = false;
  },

  dispose: function() {
    this.slider.dispose();
    this.weight.deleteObserver(this);
  },

  hex: function(v) {
    var s = (Math.round(v * 255)).toString(16);
    while (s.length < 2)
    {
      s = "0" + s;
    }
    return s;
  },

  // changes the shading of the slider's "track" to indicate
  // level (eye candy)
  updateTrackColor: function(value) {
    var rgb = hsvToRgb(0.6, 0.7, value);
    var newStyle = "#" + this.hex(rgb[0]) + this.hex(rgb[1]) + this.hex(rgb[2]);
    this.slider.track.style.background = newStyle;
  },

  // show description of current slider setting value
  updateTooltipText: function(value) {
    var idx = Math.floor(value * (Demo.Slider.descriptions.length - 1));
    var description = Demo.Slider.descriptions[idx];
    this.tooltip.innerHTML = description;
  },

  // if bool is true, shows the slider tooltip, otherwise, hides it
  showTooltip: function(bool) {
    if (bool && !this.suspended) {
      this.tooltip.style.display = "block";
    } 
    else {
      Element.hide(this.tooltip);
    }
  },
  
  // called by the Demo.weights.updateAll() method
  updateSliderWidget: function() {
    var val = this.weight.getValue();
    this.suspended = true;
    this.slider.setValue(val);
    this._update(val)
    this.suspended = false;
  },

  // callback for the Observable
  update: function(weight) {
    Log.debug("slider " + this.widget.id + "'s .update method called by Observable");
    this._update(weight.getValue());
  },

  // updates the tooltip, track color and the tooltip text
  // called after a move of the slider
  _update: function(value) {
    Log.debug("updating slider " + this.widget.id);
    // the initial update may call us with value undefined
    value = (isNumber(value) && !isUndefined(value) && !this.suspended) ? value : this.weight.getValue();
    this.updateTooltipText(value);
    this.updateTrackColor(value);
  },

  // called when the slider is in motion
  onSlide: function(value, slider) {
    Demo.UpdateListener.cancelAjax();
    this._update(value);
    this.showTooltip(true);
    Element.addClassName(this.widget, "activeSlider");
  },

  // called when the slider stops moving
  onChange: function(value, slider) { 
    if (!this.suspended) {
      this.weight.setValue(value);
      Element.removeClassName(this.widget, "activeSlider");
      this.showTooltip(false);
    }
  }
};

Demo.Slider._data = new Array();

// since we have two tabs with two sets of sliders, it is necessary to 
// create the sliders dynamicallly in repsonse to the user clicking on the
// "Browse" or "Advanced" tabs, and recreate them each time they switch back
// and forth. The slider values and dom-ids are set up during page evaluation
// by calls to Demo.Slider.register(), and the sliders are then (re)created from
// this data when necessary. There is a behaviour.js hook that connects the
// 'Browse' and 'Advanced' links to the logic that recreates the sliders.
//
Demo.Slider.ClassMethods = {
  // set up the data for a slider to be rendered and bound
  // dynamically
  //
  // arguments are: widget_id, handle_id, track_id, help_id, dimension
  register: function() {
    Demo.Slider._data.push(arguments);
  },

  _create: function(widget_id, handle_id, track_id, help_id, dimension) {
    var weight = Demo.weights.getWeight(dimension);

    var slider = new Demo.Slider(widget_id, handle_id, track_id, 
                                help_id, dimension, weight);
    slider.showTooltip(false);
    Demo.sliders[widget_id] = slider;

    slider.updateSliderWidget();
    return slider;
  },

  disposeAll: function() {
    Demo.sliders.keys().each(function(key) {
      Demo.sliders[key].dispose();
      delete Demo.sliders[key];
    });
  },

  setup: function() {
    Demo.Slider.ClassMethods.disposeAll();
    Demo.Slider._data.each(function(args) {
      Demo.Slider.ClassMethods._create.apply(null, args);
    });
  }
};

Object.extend(Demo.Slider, Demo.Slider.ClassMethods);


