/*
 * Copyright (c) 2006 Byrne Reese. All rights reserved.
 * http://www.majordojo.com/projects/javascript/filter-widget
 * 
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the BSD License.
 *
 * This library is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. 
 *
 * @author Byrne Reese <byrne@majordojo.com>
 * @version 1.0
 */

/**
 * @class The FilterWidget comprises a set of Constraints which are used to
 * in conjunction with one another to create a filter. The Constraint class
 * is used to encode the data necessary to represent a restriction that can
 * be made upon a Filter Widget.
 *
 * @param String The base ID for the widget. The baseId is appended
 * to all dynamically constructed DOM elements so that multiple filter widgets
 * can co-exist on the same page without conflicting or confusing one another.
 *
 * @constructor
 */
function Constraint( baseId ) {

  /**
   * If set to 1, then this constraint is currently applied to the containing
   * filter. If set prior to the filter widget being rendered, the constraint
   * will be rendered with the constraint active and applied to the filter.
   * @type integer
   */
  this.selected = 0;

  /**
   * If set to 0, then this constraint cannot be edited and will be rendered
   * as static text. Users will not be able to change the value, they will 
   * only be allowed to apply or un-apply the constraint to the filter.
   * @type integer
   */
  this.editable = 1;

  /**
   * The label of the constraint. This is used in rendering and displaying
   * the constraint in pull down menus and in the text version of the filter.
   * @type String
   */
  this.label = '';

  /**
   * The name of the filter as it will be submitted when a user submits the
   * filter widget. The 'name' of the constraint is analogous to the 'name'
   * of an input HTML element.
   * @type String
   */
  this.name = '';

  /**
   * The current value of the constraint. 
   * @type String
   */
  this.value = '';

  /**
   * An array of possible values for the constraint. The value of this property
   * is an array of HTML Option objects.
   * @type Option[];
   */
  this.options  = new Array();

  /**
   * Returns an exact replica and separate instance of the current constraint.
   * @return Constraint
   */
  this.clone = function() {
    var c = new Constraint();
    c.selected = this.selected;
    c.editable = this.editable;
    c.label    = this.label;
    c.name     = this.name;
    c.value    = this.value;
    c.options  = new Array();
    for (var i = 0; i < this.options.length; i++) {
      var o = this.options[i];
      c.options[i] = new Option(o.text, o.value, o.defaultSelected, o.selected);
    }
    return c;
  }
}

/**
 * @class The FilterWidget is the primary user interface widget that is
 * instantiated and manipulated by the developer. A developer would
 * instantiate a FilterWidget, and then add a set of Constraints to the 
 * widget which would define each individual and discrete filter one could
 * apply via the widget.
 *
 * @param {String} baseId The base ID for the widget. The baseId is appended
 * to all dynamically constructed DOM elements so that multiple filter widgets
 * can co-exist on the same page without conflicting or confusing one another.
 *
 * @constructor
 * @param {String} baseId The base ID for the widget. The baseId is appended
 * to all dynamically constructed DOM elements so that multiple tag widgets
 * can co-exist on the same page without conflicting or confusing one another.
 * @requires YAHOO.util.CustomEvent As provided by the Yahoo User Interface Library
 * @requires YAHOO.util.Event As provided by the Yahoo User Interface Library
 */
function FilterWidget( baseId ) {
  /**
   * Every widget needs a base id that is unique that will allow multiple
   * instances of this widget to co-exist on the same rendered page.
   * @type String
   */
  this.baseId = baseId;

  /**
   * The display name of the object that is being filtered by the widget,
   * the value of this property should be in singular form.
   * @type String
   */
  this.dataTypeLabel = '';

  /**
   * The pluralized version of the dataTypeLabel property.
   * @type String
   */
  this.dataTypeLabelp = ''; // plural form

  /**
   * The constraints array contains a list of possible constraints to be added 
   * to the filter. This array is only a list of possible constraints. It
   * does not reflect which constraints are currently applied.
   * @private
   */
  this.constraints = new Array();

  /**
   * @private
   */
  this.original = new Array(); // this holds the state so that we can undo

  /**
   * The selected array contains a list of all selected filter constraints
   * @type Array
   * @private
   */
  this.selected = new Array();

  /**
   * Adds a constraint to the list of possible constraints that can be applied
   * to the current filter.
   * @param Constraint The constraint object to add to the widget.
   */
  this.addConstraint = function ( c ) {
    // tack on the constraints
    this.constraints[this.constraints.length] = c;
    this.original = this.cloneConstraints();
  }

  /**
   * Event hook for processing a filter request
   * @type YAHOO.util.CustomEvent
   */
  this.filterEvent = new YAHOO.util.CustomEvent("filterEvent", this);

  /**
   * @private
   */
  this.constraintList;

  /**
   * @return boolean Returns true if there are any constraints currently
   * selected by the user.
   */
  this.hasSelected = function () {
    if (this.selected.length > 0) { return 1; }
    for (var i = 0; i < this.constraints.length; i++) {
      if (this.constraints[i].selected) { return 1; }
    }
    return 0;
  }

  /**
   * Function that renders the widget on the page. The function binds the
   * rendered HTML to the element identified by the baseId property.
   * This renders the widget in a human readable text form. In the text 
   * form of the widget cannot be edited.
   */
  this.renderAsText = function () {
    var node = YAHOO.util.Dom.get(this.baseId);
    if (!node) return;
    var div = document.createElement('a');
    div.href = '#';
    div.appendChild(document.createTextNode('Showing '));
    if (!this.hasSelected()) {
      div.appendChild(document.createTextNode('all '));
    }
    div.appendChild(document.createTextNode(this.dataTypeLabelp));
    if (this.hasSelected()) {
      div.appendChild(document.createTextNode(' with '));
    }
    var selectedCount = 0;
    for (var i = 0; i < this.constraints.length; i++) {
      var c = this.constraints[i];
      if (c.selected) {
        if (++selectedCount > 1) {
          div.appendChild(document.createTextNode(' and '));
        }
        div.appendChild(document.createTextNode(c.label));
        div.appendChild(document.createTextNode(' of '));
        if (c.editable) {
          for ( var o in c.options ) {        
            var opt = c.options[o];
            if (opt.selected) {
              div.appendChild(document.createTextNode(opt.innerHTML));
            }
          }
        } else {
          div.appendChild(document.createTextNode(c.value));
        }
      }
    }
    YAHOO.util.Event.addListener(div, "click", this.onFilterStringClick, this, true);
    node.appendChild(div);
    return false;
  }

  /**
   * Function that renders the widget on the page. The function binds the
   * rendered HTML to the element identified by the baseId property.
   * This renders the widget in a form that can be edited.
   */
  this.renderAsForm = function () {
    var node = YAHOO.util.Dom.get(this.baseId);
    if (!node) return;
    node.innerHTML = '';
    var f = document.createElement('form');
    f.id = this.baseId + '-form';
    var pre = document.createElement('div');
    pre.className = 'constraint';
    pre.appendChild(document.createTextNode('Showing '));

      var s1 = document.createElement('select');
      s1.id = this.baseId + "-selector";
      var def = Array('all','only');
      for ( var i in def ) {
        var o = document.createElement('option');
        o.innerHTML = def[i];
        if (o.value == 'only' && this.selected.length > 0) { o.selected = 1; }
        s1.appendChild(o);
      }
      for (var i = 0; i < this.constraints.length; i++) {
        var c = this.constraints[i];
        var o = document.createElement('option');
        o.id = this.baseId + '-' + c.name + '-option';
        o.value = c.name;
        o.innerHTML = '...with ' + c.label + ' of...';
        s1.appendChild(o);
      }
      YAHOO.util.Event.addListener(s1,"change",this.onSelectorChange,this,true);
      pre.appendChild(s1);

    pre.appendChild(document.createTextNode(" " + this.dataTypeLabelp));
    f.appendChild(pre);

    this.constraintList = document.createElement('div');
    this.constraintList.id = this.baseId + '-constraints-list';

    f.appendChild(this.constraintList);
    
    var fdiv = document.createElement('div');
    fdiv.className = "filter-buttons";
    var filter = document.createElement('input');
    filter.type = 'button';
    filter.value = 'Filter';
    var cancel = document.createElement('input');
    cancel.type = 'button';
    cancel.value = 'Cancel';
    YAHOO.util.Event.addListener(filter,"click",this.onFilter,this,true);
    YAHOO.util.Event.addListener(cancel,"click",this.onCancel,this,true);
    fdiv.appendChild(filter);
    fdiv.appendChild(cancel);
    f.appendChild(fdiv);
    node.appendChild(f);

    for (var i = 0; i < this.constraints.length; i++) {
      if (this.constraints[i].selected) {
        this.enableConstraint(i);
      }
    }
    return false;
  }

  /**
   * @private
   */
  this.onSelectorChange = function ( e ) {
    var target = YAHOO.util.Event.getTarget(e);
    var idx = target.options.selectedIndex;
    //var f = this.constraintList;
    if (target.options[idx].innerHTML == 'all') {
      //alert('Removing constraints');
      for (var i = 0;i < this.selected.length;i++) {
        var c = this.constraints[this.selected[i]];
        var idx = this.selected[i];
        this.disableConstraint(idx);
      }
      //this.selected = new Array();
      this.constraintList.innerHTML = '';
      //YAHOO.util.Dom.get(this.baseId + '-form-with').style.display = 'none';
    } else if (target.options[idx].innerHTML == 'only') {
      //alert('do nothing');
    } else {
      for (var i = 0;i < this.constraints.length; i++) {
        if (this.constraints[i].name == target.options[idx].value) {
          this.enableConstraint(i);
        }
      }
    }
  }

  /**
   * @private
   */
  this.onConstraintChange = function ( e ) {
    // TODO - use enable and disableConstraint options
    var target = YAHOO.util.Event.getTarget(e);
    var idx = target.options.selectedIndex;
    if (target.options[idx].innerHTML == 'any') {
      for (var i = 0;i < this.selected.length;i++) {
        if (this.constraints[this.selected[i]].name == target.name) {
          this.disableConstraint(this.selected[i]);
        }
      }
    }
  }

  /**
   * @private
   */
  this.onFilterStringClick = function ( e ) {
    YAHOO.util.Event.stopEvent(e);
    this.original = this.cloneConstraints(); // "commit" changes
    var node = YAHOO.util.Dom.get(this.baseId);
    node.innerHTML = '';
    return this.renderAsForm();
  }

  /**
   * @private
   */
  this.onCancel = function ( e ) {
    this.constraints = this.original; // undo changes
    var node = YAHOO.util.Dom.get(this.baseId);
    node.innerHTML = '';
    return this.renderAsText();   
  }

  /**
   * @private
   */
  this.onFilter = function ( e ) {
    var params = new Array();
    for (var i = 0; i < this.selected.length; i++) {
      var c = this.constraints[this.selected[i]];
      var node = YAHOO.util.Dom.get('constraint-val-' + c.name);
      var cmd = 'params["' + c.name + '"] = "' + node.value + '"';
      eval(cmd);
    }
    this.filterEvent.fire( params );

    var node = YAHOO.util.Dom.get(this.baseId);
    node.innerHTML = '';
    return this.renderAsText();   
  }

  /**
   * @private
   */
  this.enableConstraint = function( idx ) {
      var c = this.constraints[idx];
      var div = document.createElement('div');
      div.id = 'constraint-' + c.name;
      div.className = 'constraint';

      var prestr = document.createElement('span');
      prestr.id = this.baseId + '-' + c.name + '-prefix';
      prestr.className = 'constraint-prefix';
      if (this.selected.length == 0) {
        prestr.innerHTML = ' with ';
      } else {
        prestr.innerHTML = ' and ';
      }
      div.appendChild(prestr);

      // tack this onto the list of active constraints
      this.selected[this.selected.length] = idx;

      div.appendChild(document.createTextNode(c.label));
      div.appendChild(document.createTextNode(' of '));

      if (c.editable) {
        var s2 = document.createElement('select');
        s2.id = 'constraint-val-' + c.name
        s2.name = c.name;
        var any = document.createElement('option');
        any.innerHTML = 'any';
        s2.appendChild(any);
        for (var j = 0; j < c.options.length; j++) {
          var o = document.createElement('option');
          if (j == 0) { c.options[j].selected = 1; }
          s2.appendChild(c.options[j]);
        }
        YAHOO.util.Event.addListener(s2,"change",this.onConstraintChange,this,true);
        div.appendChild(s2);
      } else {
        div.appendChild(document.createTextNode(c.value));
      }

      this.constraintList.appendChild(div);

      YAHOO.util.Dom.get(this.baseId + '-' + c.name + '-option').style.display = 'none';
      YAHOO.util.Dom.get(this.baseId + '-selector').options[1].selected = 1;
  }

  /**
   * @private
   */
  this.disableConstraint = function( idx ) {
    var c = this.constraints[idx];
    var node = YAHOO.util.Dom.get('constraint-' + c.name);
    if (node) { this.constraintList.removeChild(node); }
    YAHOO.util.Dom.get(this.baseId + '-' + c.name + '-option').style.display = 'block';
    for (var i = 0; i < this.selected.length; i++) {
      if (this.selected[i] == idx) {
        this.selected.splice(i,1);
      }
    }
    c.selected = 0;
    if (this.constraintList.childNodes[0]) {
      this.constraintList.childNodes[0].childNodes[0].innerHTML = ' with ';
    }
  }

  /**
   * @private
   */
  this.cloneConstraints = function () {
    var a = new Array();
    for (var i = 0; i < this.constraints.length; i++) {
      a[a.length] = this.constraints[i].clone();
    }
    return a;
  }
}


