/*

Extensions to Prototype/Scriptaculous

*/

// For "registering" namespaces
// (automatically call an `initialize` method on window load)
Event.register = function(object) {
  // manage a stack of events to invoke
  if (!Event.registeredEvents) Event.registeredEvents = $A();
  if (!object['initialize']) return;
  
  Event.registeredEvents.push(object);
  
  // if the observers was already created, don't create another one
  if (Event.domLoadedObserverCreated) return;
  
  var domLoaded = function(){
    Event.registeredEvents.each(function(object){
      try{ object.initialize(); }
      catch (err){ console.log('Failed invocation because: ' + err); }
    });
  };

  document.observe('dom:loaded', domLoaded);

  Event.domLoadedObserverCreated = true;
};

Element.addMethods({
  isOrphaned: function(element){
    element = $(element);
    if (element.sourceIndex != null) return element.sourceIndex < 1; // for IE only
    if (element.id) return !element.ownerDocument.getElementById(element.id);
    return !element.descendantOf(element.ownerDocument.documentElement);
  },
  selectFirst: function(element, selector){
    var match = element.select(selector);
    if (match && match.length > 0) match = match[0];
    return match;
  },
  scrollTo: function(element, container, options){
    options = Object.extend({
      offsetY:0
    }, options || {});
    if (container){
      element = $(element);
      container = $(container);
      container.scrollTop = (element.offsetTop - element.offsetHeight) + options.offsetY;
    } else {
      element = $(element);
      var pos = Position.cumulativeOffset(element);
      window.scrollTo(pos[0], pos[1]);
    }
    return element;
  },
  morphIntoEdit: function(element){
    element.morph('height:100px', { duration: 0.5, afterFinish: function(morpher){ morpher.element.addClassName('active'); } });
  },
  morphOutOfEdit: function(element){
    element.morph('height:28px', { duration: 0.5, afterFinish: function(morpher){ morpher.element.removeClassName('active'); } });
  },
    
  // these helpers are custom to our app...
  getFirstInputValue: function(element){
    element = $(element);
    var my_inputs = element.getElementsByTagName('input');
    var input_value = 'error';
    if (my_inputs && my_inputs.length > 0){
      input_value = my_inputs[0].value;
    }
    return input_value;
  },
  text: function(element){
    element = $(element);
    /* 
    Return a node's inner text only, not the HTML. 
    IE uses one method (innerText) and all other browsers use a different one (textContent)
    Also checks for a node or empty node, since it *is* possible to have an empty node 
    */
    return (element ? (element.innerText ? element.innerText : element.textContent) : '');
  },
  visibleOnPage: function(element) {
    /* checks to see if any of the element or any of the parent's ancestors are hidden */
    element = $(element);
    var visible = element.ancestors().invoke('visible').detect(function(a) { 
      return (a == false); 
    });

    if (visible == undefined) return true;
    else return false;    
  },
  scrolledIntoView: function(element, scrollParent) {
    /* given an element and its scrolling parent, will return whether or not element is visible */
    element = $$$(element);
    scrollParent = $$$(scrollParent);
        
    var relativeTopPosition = element.cumulativeOffset()[1] - scrollParent.cumulativeOffset()[1];
    var relativeScrollPosition = relativeTopPosition - scrollParent.scrollTop;

    if (relativeScrollPosition < 0) {
      return false;
    }
    else if (relativeScrollPosition > scrollParent.getHeight()) {
      return false;      
    }
    else {
      return true;
    }    
  }
});

Event.GENERAL_SUMMARY = {
  START_EDIT:'general-summary-edit:start',
  FINISH_EDIT:'general-summary-edit:finish'
};
Event.NOTES = {
  START_EDIT:'notes-edit:start',
  FINISH_EDIT:'notes-edit:finish'
};
Event.SOFTWARE_LICENSE = {
  START_EDIT:'software-license-edit:start',
  FINISH_EDIT:'software-license-edit:finish'
};
Event.POPUP = {
  OPEN:'popup-form:open',
  CLOSE:'popup-form:close'
};
Event.TICKET = {
  START_EDIT:'ticket-edit:start',
  FINISH_EDIT: 'ticket-edit:finish'
};

// some form helpers
Object.extend(Form, {
  // this implementation is specific to Spiceworks, since every form submit button has a class of "image_button"
  getFormButtons: function(form){
    return $A(form.select('.image_button'));
  }
});

Object.extend(Form.Element, {
  clearDefaultText: function(element, defaultText){
    element = $(element);

    if ($F(element) == defaultText){
      element.removeClassName('init');
      element.value = '';
    }
    return element;
  }
});

var TextFieldWithDefault = Class.create({
  initialize: function(textField, defaultText){
    this.textField = $(textField);
    this.defaultText = defaultText;
    if ($F(this.textField) == this.defaultText) this.textField.addClassName('init');
    this.textField.observe('focus', Form.Element.clearDefaultText.curry(this.textField, this.defaultText));
  }
});

Form.Element.enable = Form.Element.enable.wrap(function(){
  var args = $A(arguments), proceed = args.shift();
  var element = proceed.apply(this, args);
  
  element.removeClassName('disabled');

  if (element.getAttribute('type') == 'image' && element.hasAttribute('key')) {
    // this is a special button that's already been instantiated as a ImageButton object
    element.removeClassName('image_button_disabled');
    ButtonManager.alterStateOfButton(element.getAttribute('key'), 'normal');
  } else {
    if (element.getAttribute('type') == 'image'){
      element.removeClassName('image_button_disabled');
      element.src = element.src.replace('_hover', '').replace('_disabled', '');
    }
  }
  return element;
});

Form.Element.disable = Form.Element.disable.wrap(function(){
  var args = $A(arguments), proceed = args.shift();
  var element = proceed.apply(this, args);

  element.addClassName('disabled');

  if (element.getAttribute('type') == 'image' && element.hasAttribute('key')) {
    // this is a special button that's already been instantiated as a ImageButton object
    element.addClassName('image_button_disabled');
    ButtonManager.alterStateOfButton(element.getAttribute('key'), 'disabled');
  } else {
    if (element.getAttribute('type') == 'image') {
      element.addClassName('image_button_disabled');
      element.src = element.src.replace('_hover', '').replace('_disabled', '');
      element.src = element.src.replace(/\.gif/, '_disabled.gif');
    }
  }
  return element;
});

var Pulsator = Class.create();
Pulsator.prototype = {
  initialize: function(options) {
    this.options = Object.extend({
      index:0,
      duration:1,
      from:0,
      pulses:2,
      color:'#FE5200',
      border:5
    }, options || {});
    
    if(this.options.element_id){
      this.element = $(this.options.element_id);
    }
    else if(this.options.selector){
      this.element = $$(this.options.selector)[this.options.index];
    }
    
    if(this.element){
      this.node = $(document.createElement('div'));
      document.body.appendChild(this.node);
      this.node.setStyle({
        position:'absolute',
        border:this.options.border + 'px solid ' + this.options.color
      });
      Position.clone(this.element, this.node, {offsetLeft:1-this.options.border, offsetTop:1});
      this.node.pulsate({
        duration: this.options.duration,
        from: this.options.from,
        pulses: this.options.pulses,
        afterFinish: function() {
          this.node.fade({duration:0.5});
          this.activate();
        }.bind(this)
      });
    } else{
      this.activate();
    }
  },
  
  activate: function(){
    if (this.options.onclick) {
      this.options.onclick();
    } else if (this.options.url) {
      window.location.href = this.options.url;
    }
  }
};

Ajax.InPlaceEditor.Autocompleter = {};
Ajax.InPlaceEditor.Autocompleter.Local = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, updater, array, autocompleter_options, options){
    $super(element, url, options);
    this.updater = $(updater);
    this.options.array = array;
    this.autocompleter_options = autocompleter_options || {};
  },
  handleFormSubmission: function($super, e){
    if (!this.autocompleter.active) $super(e);
  },
  createEditField: function($super){
    $super();
    if (!this.autocompleter) this.autocompleter = new Autocompleter.Local(this._form.select('input.editor_field').first(), this.updater, this.options.array, this.autocompleter_options);
  }
});

var SortableTable = Class.create();
SortableTable.prototype = {
  initialize:function(table, manager, options) {
    this.table = $(table);
    this.thead = this.table.getElementsByTagName('thead')[0];
    this.tbody = this.table.getElementsByTagName('tbody')[0];
    this.options = Object.extend({
      clickable:       false,
      striped:         true,
      evenStripeClass: "stripe0",
      oddStripeClass:  "stripe1"
    }, options || {});

    this.sort_columns = $A(this.thead.getElementsByTagName('td')).collect(function(elem, index) {
      elem.sort_index = index;
      // elem.ascending  = elem.className.include('sorted');
      
      if (Browser.ie6){
        Element.observe(elem, 'mouseover', function(e){ e.findElement('td').addClassName('hover'); });
        Element.observe(elem, 'mouseout', function(e){ e.findElement('td').removeClassName('hover'); });
      }
      Event.observe(elem, "click", this.sort_column.bindAsEventListener(this));
      return {
        sort_function: manager.sort_function(elem),
        node: elem
      };
    }.bind(this));
    this.current_sort_col = this.sort_columns[0].node;

    var trs = null;
    trs = this.cacheRows();

    if (this.options.clickable) {
      this.tbody.className = "clickable";
      // use event delegation to cut down on looping
      Event.observe(this.tbody, 'click', this.clickRowManager.bindAsEventListener(this));
    }

    if (Browser.ie6){
      // use event delegation to cut down on looping
      this.table.observe('mouseover', this.addHoverManager);
      this.table.observe('mouseout',  this.removeHoverManager);      
    }

  },

  // cache all of the rows of the table (initialization primarily)
  cacheRows: function(){
    var trs   = [];
    this.rows = [];
    // raw loop for speed
    var rows = this.tbody.getElementsByTagName('tr');
    for (var i = 0, row; row = rows[i]; i++) {
      trs.push(row);
      var tds = row.getElementsByTagName('td'), cells = [];

      for (var j = 0, cell; cell = tds[j]; j++){
        var sf = null;
        var sort_col = null;

        sort_col = this.sort_columns[j];
        sf = sort_col.sort_function(cell);
        cells.push(sf);
      }

      this.rows.push({ sort_values: cells, node: row });
    }
    return trs;
  },

  // cache a single row.
  cacheRow: function(element, recache) {
    var cells = $A(element.getElementsByTagName("td")).collect(function(cell, index) {
      return this.sort_columns[index].sort_function(cell);
    }.bind(this));

    // look for the row in the cached rows
    found = false;
    this.rows.each(function(row) {
      // if found, replace the sort_values and the element
      if(row.node == element){
        found = true;
        row.sort_values = cells;
      }
    }.bind(this));

    // If the row wasn't found, then it's new and we need to add it.
    // and also add listeners for clicks.
    if(!found) {
      this.rows[this.rows.length] = {
        sort_values:cells,
        node:element
      };

      if (this.options.clickable) {
        Event.observe(element, 'click', this.clickRow.bindAsEventListener(this));
      }
      if (Browser.ie6){
        Event.observe(element, 'mouseover', this.addHoverManager);
        Event.observe(element, 'mouseout', this.removeHoverManager);      
      }
    }
  },
  removeRow:function(row_to_remove){
    element = $(row_to_remove);
    element_to_select = element.next();

    var cached_row = null;
    this.rows.each(function(row){
      if(row.node == element){
        cached_row = row;
      }
    }.bind(this));
    this.rows = this.rows.without(cached_row);
    Element.remove(element);
    // if the element was clicked, then select another row
    if(!element_to_select && this.rows.size() > 0){
      element_to_select = this.rows.first().node;
    }

    if(Element.hasClassName(element,'clicked')){
      this.options.clickHandler(element_to_select);
    }
  },
  
  clickRow:function(event) {
    var clicked_element = event.element();
    if(this.options.clickHandler && !clicked_element.tagName.toLowerCase().match(/input|a/)) {
      this.options.clickHandler(event.findElement('tr'));
    }
  },
  selectRow:function(element) {
    this.options.clickHandler($(element));
  },
  
  clickRowManager: function(e) {
    var tr = e.findElement('tr'), element = e.element(), opt = this.options;
    if (!tr || !element) return;
    if (opt.clickHandler && !$w('INPUT A TBODY').include(element.tagName.toUpperCase())) opt.clickHandler(tr);
  },
  
  addHoverManager: function(e) {
    var element = e.findElement('tr');
    if (element && element.className && !element.className.include('hover')) Element.addClassName(element, 'hover');
  },
  removeHoverManager: function(e) {
    var element = e.findElement('tr');
    if (element && element.className && element.className.include('hover')) Element.removeClassName(element, 'hover');
  },
  headerMouseOver: function(e){
    var cell = e.findElement('td');
    cell.addClassName('hover');
  },
  headerMouseOut: function(e){
    var cell = e.findElement('td');
    cell.removeClassName('hover');
  },
  
  // Called when someone actually clicks on a column header
  sort_column:function(event) {
    var col = event.element();
    this.setSortDirection(col);
    this.do_sort(col);
  },
  
  // Method which actually performs the sort on a given column.
  do_sort:function(col) {
    var result = this.rows.sortBy(function(row) {
      return row.sort_values[col.sort_index];
    });
    if(Element.hasClassName(col, "desc")){
      result = result.reverse();
    }
    this.drawSortResult(result);
    this.current_sort_col = col;
  },

  // Refresh the sort without changing anything (call after a new row is added to the table)
  refresh_sort:function() {
    this.do_sort(this.current_sort_col);
  },

  setSortDirection:function(sorted_column, direction) {
    var ascending = true; // default to ascending

    sorted_column = $(sorted_column) || this.current_sort_col;
    
    // if we're looking at a date, then make the default descending
    if(sorted_column.hasClassName('default_sort:desc') || sorted_column.hasClassName('sort:date')){
      ascending = false;
    }
    
    if (direction){
      ascending = (direction === 'desc' ? false : true);
    }else if( this.current_sort_col === sorted_column ) {
      /* Flip the sort order if it's the current column and we're ascending */
      if( sorted_column.hasClassName('asc') ) {
        ascending = false;
      }else{
        ascending = true;
      }
    }
    
    // using traditional loops b/c they're faster
    for (var i = 0, cell; i < this.sort_columns.length; i++) {
      cell = this.sort_columns[i].node;
      $(cell).removeClassName('sorted').removeClassName('asc').removeClassName('desc');
    }
    sorted_column.addClassName("sorted " + (ascending ? "asc" : "desc"));
  },

  drawSortResult: function(result) {
    var opt = this.options, row, even;
    // using traditional loops b/c they're faster
    for (var index = 0, len = result.length, row, even; index < len; index++) {
      row = result[index].node;
      if (opt.striped) {
        even = (index % 2) == 0;
        if (even && Element.hasClassName(row, opt.oddStripeClass)) {
          row.className = row.className.replace(opt.oddStripeClass, opt.evenStripeClass);
        } else if (!even && Element.hasClassName(row, opt.evenStripeClass)) {
          row.className = row.className.replace(opt.evenStripeClass, opt.oddStripeClass);
        }
      }
      this.tbody.appendChild(row);
    }
  },
  
  destroy: function(){
    $A(this.thead.getElementsByTagName('td')).each(function(elem, index) {
      if (Browser.ie6){
        Element.stopObserving(elem, 'mouseover', function(e){ e.findElement('td').addClassName('hover'); });
        Element.stopObserving(elem, 'mouseout', function(e){ e.findElement('td').removeClassName('hover'); });
      }
      Event.stopObserving(elem, "click", this.sort_column.bindAsEventListener(this));
    }.bind(this));
    this.sort_columns = null;

    this.rows.each(function(element){
      if (this.options.clickable) {
        Event.stopObserving(element, 'click', this.clickRow.bindAsEventListener(this));
      }
      if (Browser.ie6){
        Event.stopObserving(element, 'mouseover', this.addHoverManager);
        Event.stopObserving(element, 'mouseout', this.removeHoverManager);      
      }
    }.bind(this));

    if (this.options.clickable) Event.stopObserving(this.tbody, 'click', this.clickRowManager.bindAsEventListener(this));

    if (Browser.ie6){
      // use event delegation to cut down on looping
      this.table.stopObserving('mouseover', this.addHoverManager);
      this.table.stopObserving('mouseout',  this.removeHoverManager);      
    }

    this.table = null;
    this.thead = null;
    this.tbody = null;
    this.rows = null;
  },
  isOrphaned: function(){ return this.table.isOrphaned(); }
};

var SortableTableManager = new Object();
Object.extend(SortableTableManager, {
  initialize: function(){
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
    this._attachFresh();
  },
  register_sortables: function(){ this._attachFresh(); },
  ajaxOnComplete: function(){
    SortableTableManager._removeOrphaned();
    SortableTableManager._attachFresh();
  },
  _removeOrphaned: function(){
    SortableTableManager.registered_sortables.each(function(pair){
      if (pair.value.isOrphaned()){
        pair.value.destroy();
        SortableTableManager.registered_sortables.unset(pair.key);
      }
    });
  },
  _attachFresh: function(){
    if (!SortableTableManager.registered_tables) SortableTableManager.registered_tables = [];
    if (!SortableTableManager.registered_sortables) SortableTableManager.registered_sortables = $H();
    
    $$('table.sortable').each(function(element) {
      if (!SortableTableManager.registered_tables.include(element)) {
        SortableTableManager.register_sortable(element);
        SortableTableManager.registered_tables.push(element);
      }
    }.bind(SortableTableManager));
  },
  register_sortable: function(element) {
    var options = {};
    var click_handler = element.className.match(/clickable:(.*) {0,1}.*/);
    if (click_handler) {
      options = {
        clickable: true,
        clickHandler: this.click_functions[click_handler[1]]
      };
    }
    SortableTableManager.registered_sortables.set(element, new SortableTable(element, this, options));
  },
  sort_function: function(element) {
    // We are expecting the className of the passed element to include a hint in the format
    // sort:(strategy). If we can't find the sort_function, assume string
    var className = null;
      className =  element.className.match(/sort:(\w*) {0,1}\w*/);
      className = className ? className[1] : "string";
      return this.sort_functions[className] || this.sort_functions.stringSort;
  },
  sort_functions: {
    stringSort: function(element) {
      return element.innerHTML.stripTags().toLowerCase();
    },
    
    versionSort: function(element) {
      var value = element.title || ""; // compare against the literal value
      if (value == "")  return -1;     // empty strings get sorted at the end
      
      // catch values like "v3.6.3" or "V 3.6.3"
      if ((/^\s*v\s*\d/i).test(value)) value = value.substring(1, value.length);
      else if (!(/^\d/).test(value)) return 0;
      
      // split it into tokens (["3", "6", "3"])
      var tokens = value.split('.').slice(0, 4);
      // pad it (["3", "6", "3", "00000"])
      while (tokens.length < 4) tokens.push('00000');
      tokens = tokens.map(function(token) {
        if (token.length > 5) token = token.substring(0, 5);
                
        // pad each token (["00003", "00006", "00003", "00000"])
        if (token.length < 5) token = ('0').times(5 - token.length) + token;
        return token;
      });
      
      // join it and convert it to a number (3000060000300000)
      return parseInt(tokens.join(''), 10);
    },
    
    full_name: function(element) {
      var sort_value = element.innerHTML.stripTags().toLowerCase();
      return sort_value.replace(/^(.*) (.*)$/, "$2 $1");
    },
    bytes: function(element) {
      var sort_value = element.innerHTML.stripTags().toLowerCase();
      if (result = /^(.*) (k|m|g)B/i.exec(sort_value)) {
        return parseFloat(result[1]) * (result[2] == "m" ? (1024 * 1024) : (result[2] == "g" ? (1024 * 1024 * 1024) : 1024));
      } else {
        return 0;
      }
    },
    numeric: function(element) {
      var sort_value = element.innerHTML.stripTags().toLowerCase();
      var result = parseFloat(sort_value.gsub(/\$/, '')); /* Strip out dollar sign for currency */
      // NaN values should return -1 so that they can be distinguished from 0
      return isNaN(result) ? -1 : result;
    },
    date: function(element) {
      if (element.getAttribute("millis")) {
        return new Date(parseInt(element.getAttribute("millis")));
      }
      var sort_value = element.innerHTML;
      if(date = sort_value.match(/(\d+)\/(\d+)\/(\d+) @ (\d+):(\d+)([ap]m)/)){
        /* finder_date_time format */
        var hour = parseInt(date[4], 10);
        if(date[6] == 'pm'){hour += 12;}
        if(hour == 12 || hour == 24){hour -= 12;}
        return Date.UTC(date[3], date[1], date[2], hour, date[5], 0);
      }else{
        return Date.parse(sort_value.stripTags());
      }
    },
    ticket_priority: function(element){
      if(element == null)return 2; // Assume 2 if there is not column.
      var priority_hash = {'high':3, 'med':2, 'medium':2, 'low':1};
      priority = priority_hash[element.innerHTML.toLowerCase()];
      return priority;
    },
    // This is the default sort order.  status/priority/id
    ticket_externally_updated: function(element){
      var retval = "2";
      try{
          if(Element.hasClassName(element, 'past_due')){
            retval = "0";
          }else if(Element.hasClassName(element, 'externally_updated')){
            retval = "1";
          }else if(Element.hasClassName(element, 'closed')){
            retval = "3";
          }

        var id = null;
        id = element.id.split("_")[4];
        var priority_elem = null;
        var priority = null;

        // Sort first by past_due/externally_updated then by priority, then id
        priority_elem = $('ticket_table_priority_' + id);
        priority = SortableTableManager.sort_functions.ticket_priority(priority_elem);
        retval = retval + (3 - priority);
        
        retval = retval + id;
      }catch(ex){}
      return retval;
    },
    ip_address: function(element) {
      var sort_value = element.innerHTML.stripTags();
      var result = sort_value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/);
      if (result != null) {
        result = result.slice(1).collect(function(elem, idx) {
          switch(elem.length) {
            case 1:
              return "00" + elem;
            case 2:
              return "0" + elem;
            default:
              return elem;
          }
        });
        return result.join(".");
      } else {
        return sort_value;
      }
    }
  },
  click_functions: {
    software_table: function(row) {
      software_table.row_click(row);
    },
    ticket_table: function(row) {
      Ticket.selectTicket(row);
    },
    edit_ticket: function(row) {
      var edit_url = row.getAttribute('edit_url');
      document.location= edit_url;
    },
    attachment_table:function(row){
      document.location = row.down('a').href;
    }
  }
});

Event.register(SortableTableManager);

var ClickableTable = Class.create({
  initialize: function(table, options){
    this.options = Object.extend({
    }, options || {});
    this.table = $(table);
    
    this._boundClickListener = this.rowClick.bindAsEventListener(this);
    this._boundMouseOver = this.rowMouseOver.bindAsEventListener(this);
    this._boundMouseOut = this.rowMouseOut.bindAsEventListener(this);
    this._boundMouseDown = this.rowMouseDown.bindAsEventListener(this);
    this._boundMouseUp = this.rowMouseUp.bindAsEventListener(this);
    this._renderListeners('observe');
  },
  isOrphaned: function(){ return this.table.isOrphaned(); },
  destroy: function(){ this._renderListeners('stopObserving'); },

  rowClick: function(event){
    var elements = this._releventElements(event);
    
    // don't render the click action if the clicked element is in our exception list
    if (!$w('input select a').detect(function(clickedTag, exception){
      return clickedTag == exception;
    }.curry(elements.clicked.tagName.toString().toLowerCase()))) this._click(elements.row);
  },
  rowMouseDown: function(event){
    var elements = this._releventElements(event);
    elements.row.addClassName('down');
  },
  rowMouseUp: function(event){
    var elements = this._releventElements(event);
    elements.row.removeClassName('down');
  },
  rowMouseOver: function(event){
    var elements = this._releventElements(event);
    elements.row.addClassName('hover');
  },
  rowMouseOut: function(event){
    var elements = this._releventElements(event);
    elements.row.removeClassName('hover');
    elements.row.removeClassName('down');
  },

  _click: function(row){
    var clickAction = this._extractClickAction(row);
    this.table.select("tr").invoke("removeClassName", "clicked");
    row.addClassName("clicked");

    if(!clickAction.url) return;

    if (clickAction.ajax) new Ajax.Request(clickAction.url);
    else location.href = clickAction.url;
  },
  _renderListeners: function(method){
    this.table.select('tr:not([class~=not-clickable])').each(function(row){
      row[method]('click', this._boundClickListener);
      
      // do this for all browsers, since since we want to be able to write CSS that only styles clickable rows when hovered vs. using the :hover pseudo class
      row[method]('mouseover', this._boundMouseOver);
      row[method]('mouseout', this._boundMouseOut);
      row[method]('mousedown', this._boundMouseDown);
      row[method]('mouseup', this._boundMouseUp);
    }.bind(this));
  },
  _extractClickAction: function(row){
    var clickAttribute = row.getAttribute('click').evalJSON();
    return { ajax: (clickAttribute.ajax || false), url: clickAttribute.url };
  },
  _releventElements: function(event){ return { clicked: event.element(), row: event.findElement('tr') }; }
});

var ClickableTableManager = {
  initialize: function(){
    if (this.initialized) return;
    this.tables = $H();
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
    this._attachNew();
    this.initialized = true;
  },
  ajaxOnComplete: function(){
    this._removeOrphaned();
    this._attachNew();
  },
  _removeOrphaned: function(){
    this.tables.each(function(pair){
      if (pair.value.isOrphaned()){
        pair.value.destroy();
        this.tables.unset(pair.key);
      }
    }.bind(this));
  },
  _attachNew: function(){
    $$('table.clickable').each(function(table){
      if (table.id && !this.tables.get(table.id)) this.tables.set(table.id, new ClickableTable(table));
    }.bind(this));
  }
};

Event.register(ClickableTableManager);

var ReorderableTable = Class.create({
  initialize: function(table){
    this.table = table;
    this.tbody = table.down('tbody');

    var that = this; // so we don't have to do a binding in the iterator or other places where this is used below

    Object.extend(this.table, {
      reorderable: function(){ return that; } // add a method so we can go from the element to this instantiated object
    });
    
    this.listeners = {
      moveUpClick: this.moveUp.bindAsEventListener(this),
      moveDownClick: this.moveDown.bindAsEventListener(this),
      rowRemovedFromDOM: this.rowRemovedFromDOMCallback.bindAsEventListener(this)
    };

    var rows = this.table.select('tbody tr');
    rows.each(this.prepareRow.bind(this).curry(rows));
    
    document.observe('table-row:removed-from-dom', this.listeners.rowRemovedFromDOM);
  },
  rowAdded: function(newRow){
    var allRows = this.table.select('tbody tr');
    this.prepareRow(allRows, newRow, allRows.size()-1, {newRow:true});
    this.checkUpDown();
  },
  prepareRow: function(allRows, moveableRow, index, options){
    options = options || {newRow:false};
    var moveUp, moveDown;
    moveUp = moveableRow.down('a.move-up');
    moveDown = moveableRow.down('a.move-down');
    moveUp.observe('click', this.listeners.moveUpClick);
    moveDown.observe('click', this.listeners.moveDownClick);
    
    if (!options.newRow){
      this._switchMoveUpDown(moveUp, 'up', index != 0);
      this._switchMoveUpDown(moveDown, 'down', index != allRows.size() - 1);
    }
  },
  moveUp: function(event){
    if (event.element().hasClassName('move-up-disabled')) { return event.stop(); }

    var rowToMove = event.findElement('tr');
    // don't do the IE method for now, it is broken, see trac #10511
    if (false && this.tbody.moveRow && rowToMove.rowIndex){
      // IE supports the proprietary moveRow method on a table element, which is faster than doing the insertBefore call
      // this currently results in a bizarre exception in IE
      this.tbody.moveRow(rowToMove.rowIndex, rowToMove.rowIndex - 1);
    } else {
      var rowToInsertBefore = rowToMove.previous('tr');
      var newRowPosition = rowToMove.parentNode.insertBefore(rowToMove, rowToInsertBefore);
    }

    this.checkUpDown();
  },
  moveDown: function(event){
    if (event.element().hasClassName('move-down-disabled')) { return event.stop(); }

    var rowToMove = event.findElement('tr');
    // don't do the IE method for now, it is broken, see trac #10511
    if (false && this.tbody.moveRow && rowToMove.rowIndex){
      // IE supports the proprietary moveRow method on a table element, which is faster than doing the insertBefore call
      // this currently results in a bizarre exception in IE
      this.tbody.moveRow(rowToMove.rowIndex, rowToMove.rowIndex + 1);
    } else {
      var rowToInsertBefore = rowToMove.next('tr').nextSibling;
      var newRowPosition = rowToMove.parentNode.insertBefore(rowToMove, rowToInsertBefore);
    }
    
    this.checkUpDown();
  },
  destroy: function(){
    this.table.reorderable = null;
  },
  checkUpDown: function(){
    var rows = this.table.select('tbody tr'), moveUp, moveDown;
    var that = this; // to avoid needing to bind
    rows.each(function(moveableRow, index){
      moveableRow.removeClassName('stripe1').removeClassName('stripe0').addClassName(index % 2 == 0 ? 'stripe1' : 'stripe0');
      moveUp = moveableRow.down('a.move-up');
      moveDown = moveableRow.down('a.move-down');
      
      that._switchMoveUpDown(moveUp, 'up', index != 0);
      that._switchMoveUpDown(moveDown, 'down', index != rows.size() - 1);
    });
  },
  _switchMoveUpDown: function(element, upOrDown, enableControl){
    var classAsString = 'move-' + upOrDown + '-disabled'; 
    if (enableControl){
      if (element.getAttribute('enabled_title')) element.title = element.getAttribute('enabled_title');
      element.removeClassName(classAsString).disabled = false;
    } else {
      var title = element.getAttribute('title');
      element.setAttribute('title', element.getAttribute('disabled_title'));
      element.setAttribute('enabled_title', title);
      element.addClassName(classAsString).disabled = true;
    }
  },
  removeRowCallback: function(row){
    // this is called before the row is actually removed from the DOM
    if (!row) return;

    // remove all attached observers
    row.down('a.move-up').stopObserving();
    row.down('a.move-down').stopObserving();
  },
  rowRemovedFromDOMCallback: function(row){
    // this is called after the row is removed from dom
    this.checkUpDown();
  }
});

ReorderableTable.initialize = function(){
  var that = this;
  if (!this.tables) this.tables = $A();
  $$('table.reorderable').each(function(table){
    // don't instantiate tables that have already been added
    if (!table.reorderable) that.tables.push(new ReorderableTable(table));
  });

  // the initialize method is setup to be called multiple times, but we only want to setup the observers once
  if (!this.observersSetup) {
    document.observe('table-row:added', function(event){
      var row = $(event.memo), table = row.up('table');
      if (table.reorderable) table.reorderable().rowAdded(row);
    });
    document.observe('table-row:removed', function(event){
      var row = $(event.memo);
      if (!row) return; // just in case this event is fired multiple times
      var table = row.up('table');
      if (table.reorderable) table.reorderable().removeRowCallback(row);
    });
    this.observersSetup = true;
    Event.observe(window, 'unload', this.pageUnload.bindAsEventListener(this));
  }
};
ReorderableTable.canMoveRowUp = function(moveControl){ return !$(moveControl).hasClassName('move-up-disabled'); };
ReorderableTable.canMoveRowDown = function(moveControl){ return !$(moveControl).hasClassName('move-down-disabled'); };
ReorderableTable.pageUnload = function(){
  this.tables.each(function(table){
    table.destroy();
  });
};

Event.register(ReorderableTable);

var EditableTable = Class.create({
  initialize: function(table){
    this.table = table;
    this.tbody = this.table.down('tbody');
    this.tfoot = this.table.down('tfoot');

    var that = this; // so we don't have to bind to this

    Object.extend(this.table, {
      editable: function(){ return that; } // add a method so we can go from the element to this instantiated object
    });

    var options = this.table.getAttribute('editable_options') || '{}';
    this.options = Object.extend({
      deleteAllRows:true,
      newURL:'new'
    }, options.evalJSON());

    this.listeners = {
      addClick: this.addRow.bindAsEventListener(this)
    };

    this.addNewLink = this.table.down('tfoot div.add-new');
    this.addNewLink.down('a').observe('click', this.listeners.addClick);

    this.addNewForm = this.table.down('tfoot div.new-form');
  },
  editRow: function(firedLink, editURL){
    var row = firedLink.up('tr'), that = this;
    this.table.select('tr.editing').each(function(row){ that._returnRowToNormal(row); });
    var params = {element:this._insertEditRow(row)};
    new Ajax.Request(editURL, {parameters:params, skipApplicationLocking:true});
    row.addClassName('editing');
    this.tbody.addClassName('editing');
    this.tfoot.addClassName('editing');
    document.fire('editable-table:start-edit', row);
  },
  saveEdit: function(event){
    var row = (event ? event.findElement('tr') : this.table.down('tr.editing'));
    document.fire('editable-table:save-edit', row);
  },
  cancelEdit: function(event){
    var row = (event ? event.findElement('tr') : this.table.down('tr.editing'));
    this._returnRowToNormal(row);
    document.fire('editable-table:cancel-edit', row);
  },
  editSaved: function(row){
    this._returnRowToNormal($(row));
  },
  addRow: function(event){
    event.stop();
    
    if (this.tbody.hasClassName('editing')) return; // do not allow new rows to be added while a row is being edited
    
    this.addNewForm.update('<h3 class="loading"><img src="/images/icons/ajax_busy.gif" alt="Busy" width="20" height="20" /> Loading' +
                           '<span>(<a href="#" onclick="return EditableTable.cancelNew(this)">cancel new</a>)</span>' +
                           '</h3>');
    this.addNewLink.hide();
    this.addNewForm.show();
    this.tbody.addClassName('adding');
    this.tfoot.addClassName('adding');
    new Ajax.Request(this.options.newURL, { parameters:{element:this.addNewForm.identify()}, skipApplicationLocking:true });
  },
  rowAdded: function(){
    this.restripe();
    this._returnAddNewRowToNormal();
  },
  newRowShown: function(){
    document.fire('editable-table:add-row');
  },
  cancelNew: function(event){
    if (event) event.stop();
    this._returnAddNewRowToNormal();
  },
  canDelete: function(confirmation){
    if (!this.options.deleteAllRows && this.tbody.select('tr').size() == 1) return false;
    return (confirm(confirmation));
  },
  removeRowCallback: function(row){
    if (!row) return;
    row.remove();
    document.fire('table-row:removed-from-dom', row);
    this.restripe();
  },
  destroy: function(){
    this.table.editable = null;
  },
  restripe: function(){
    this.tbody.select('tr').each(function(row, index){
      row.removeClassName('stripe1').removeClassName('stripe0').addClassName(index % 2 == 0 ? 'stripe1' : 'stripe0');
    });
  },
  
  _insertEditRow: function(editingRow){
    var cellID = 'edit-row-' + this.table.id, editRow = '<tr class="edit-row">' + 
                  '<td colspan="' + editingRow.select('td').length + '">' + 
                  '<div id="' + cellID + '" class="wrapper">' + 
                  '<h3 class="loading"><img src="/images/icons/ajax_busy.gif" alt="Busy" /> Loading' +
                  '<span>(<a href="#" onclick="return EditableTable.stopEdit(this)">cancel edit</a>)</span>' +
                  '</h3>' +
                  '</div>' +
                  '</td></tr>';
    editingRow.insert({after:editRow}).hide();

    return cellID; // this is used to send as a parameter to Ajax so the server knows what element to update
  },
  _returnRowToNormal: function(row){
    var editForm = this.tbody.down('tr.edit-row');
    if (editForm){
      editForm.remove();
    }
    row.show();
    row.removeClassName('editing');
    this.tbody.removeClassName('editing');
    this.tfoot.removeClassName('editing');
  },
  _returnAddNewRowToNormal: function(){
    this.addNewForm.hide().update('');
    this.addNewLink.show();
    this.tbody.removeClassName('adding');
    this.tfoot.removeClassName('adding');
  }
});

EditableTable.initialize = function(){
  var that = this;
  if (!this.tables) this.tables = $A();
  $$('table.editable').each(function(table){
    // don't instantiate tables that have already been added
    if (!table.editable) that.tables.push(new EditableTable(table));
  });

  // the initialize method is setup to be called multiple times, but we only want to setup the observers once
  if (!this.observersSetup) {
    document.observe('table-row:added', function(event){
      var row = $(event.memo), table = row.up('table');
      if (table.editable) table.editable().rowAdded();
    });
    document.observe('table-row:removed', function(event){
      var row = $(event.memo);
      if (!row) return; // just in case this event is fired multiple times
      var table = row.up('table');
      if (table.editable) table.editable().removeRowCallback(row);
    });
    this.observersSetup = true;
    Event.observe(window, 'unload', this.pageUnload.bindAsEventListener(this));
  }
};
EditableTable.cancelNew = function(activator){
  $(activator).up('table').editable().cancelNew();
  return false;
};
EditableTable.canDelete = function(deleteLink, confirmation){
  return $(deleteLink).up('table').editable().canDelete(confirmation);
};
EditableTable.editRow = function(editLink, editURL){
  editLink = $(editLink);
  editLink.up('table').editable().editRow(editLink, editURL);
  return false;
};
EditableTable.saveEdit = function(button){
  $(button).up('table').editable().saveEdit();
  return false;
};
EditableTable.stopEdit = function(button){
  $(button).up('table').editable().cancelEdit();
  return false;
};
EditableTable.pageUnload = function(){
  this.tables.each(function(table){
    table.destroy();
  });
};

Event.register(EditableTable);

var DynamicScriptInclude = {
  load: function(source, nocache){
    if (typeof nocache == 'undefined') nocache = true;
    this._remove(source);
    this._require(source, nocache);
  },
  _remove: function(source){
    // find our special script and rip it out of the page
    $$('script[src]').each(function(s){
      if (s.src.indexOf(source) > -1) s.parentNode.removeChild(s);
    });
  },
  _require: function(source, nocache){
    var js = document.createElement('script');
    js.setAttribute('language', 'javascript');
    js.setAttribute('type', 'text/javascript');
    // append a querystring value that is always changing to this script is never cached
    source = (source.match(/\?/) ? source + '&' : source + '?') + (nocache ? 'nocache=' + new Date().getTime() + '&' : '');
    js.setAttribute('src', source);
    $$('head').first().appendChild(js);
  }
};

var DynamicStylesheetInclude = {
  load: function(source, options){
    this.options = {
      nocache: false,
      media: 'all'
    };
    Object.extend(this.options, options || {});
    
    this._remove(source);
    this._require(source, this.options.nocache, this.options.media);
  },
  _remove: function(source){
    // find our special link tag and rip it out of the page
    $$('link[rel=stylesheet]').each(function(s){
      if (s.href.indexOf(source) > -1) s.parentNode.removeChild(s);
    });
  },
  _require: function(source, nocache, media){
    var css = document.createElement('link');
    css.setAttribute('rel', 'stylesheet');
    css.setAttribute('type', 'text/css');
    css.setAttribute('media', media);
    // append a querystring value that is always changing to this script is never cached
    source = (source.match(/\?/) ? source + '&' : source + '?') + (nocache ? 'nocache=' + new Date().getTime() + '&' : '');
    css.setAttribute('href', source);
    $$('head').first().appendChild(css);
  }
};

// Workaround for IE for adding a <style> directly to <head> given CSS as a string
var CSSLoader = {
  load:function(cssText){
    var styleNode = document.createElement('style');
    styleNode.setAttribute("type", "text/css");
    if (styleNode.styleSheet) { // workaround for IE
      styleNode.styleSheet.cssText = cssText;
    } else if (Prototype.Browser.WebKit) { 
      styleNode.innerText = cssText;
    } else { // DOM
      styleNode.update(cssText);
    }
    $$('head').first().appendChild(styleNode);
  }
};

// For making XHR requests that get passed up to the Community
var Delegate = {
  encode:function(communityPath){
    return '/frontendclient/delegate?frontend_path=' + encodeURIComponent(communityPath);
  }
};

Ajax.Responders.register({
  onCreate: function(request){
    document.fire('ajax:started', request); // fire a custom event when an ajax request is started

    // This will ensure that all AJAX posts have an authenticity token so we won't
    // cause rails to throw an ActionController::InvalidAuthenticityToken exception.
    if (request.method == 'post' && Application.authenticityToken) {
      // If we don't have a postBody, force one.  This is our only chance
      // to change what gets posted, because Ajax.Request will always use
      // the postBody if present and it's too late to add to request.options.parameters.
      if (!request.options.postBody)
        request.options.postBody = Object.toQueryString(request.options.parameters);

      if (!request.options.postBody.match(/authenticity_token/))
        request.options.postBody += "&authenticity_token=" + Application.authenticityToken;
    }
  },
  onComplete: function(request){
    document.fire('ajax:completed', request); // fire a custom event when an ajax request is completed for observers
  }
});

// A nice feature to allow you to load up stuff from the community easily.
Ajax.Request.prototype.request = Ajax.Request.prototype.request.wrap(function(proceed, url){
  if (url && url.startsWith('community:')) proceed(Delegate.encode(url.sub('community:','')));
  else proceed(url);
});

// Removes pairs that have null or undefined values
Hash.addMethods({
compact: function() {
    var hash = this.clone();
    hash.each(function(pair) {
      if ((!pair.value) || (typeof(pair.value) == "undefined") || (pair.value == null) || (typeof(pair.value) == "null")) {
        hash.unset(pair.key);
      }
    });
    return hash;
 }
});

function $$$(selector) {
    return ($(selector) || $$(selector).first() || null);
}

Effect.ScrollToPosition = function(x,y) {
  var options = arguments[1] || { },
    scrollOffsets = document.viewport.getScrollOffsets(),
    elementOffsets = [x,y],
    max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();  

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1] > max ? max : elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()); }
  );
};

