/*

Extensions to Prototype/Scriptaculous

*/

// In the event that the Spiceworks framework is not loaded, here are some essential functions to get us through the day
// This file relies on the Spiceworks framework in a few places, yet this file is used in places where the framework is not included
var SPICEWORKS = { observe: function(){ document.observe.apply(document, arguments); }, fire: function(){ document.fire.apply(document, arguments); }};

var Browser = { firefox:false, safari:false, ie8: false, ie7:false, ie6:false,  
  ff3:navigator.userAgent.toLowerCase().indexOf('firefox/3') > -1, // ff3 is annoying enough to warrant browser sniffing
  hideIncompatible:function(){
    var container = $('incompatible');
    if ( container && Cookie.checkSupport() && !Browser.ie6){
      container.hide();
      SPICEWORKS.fire('browser:compatible');
      document.observe('dom:loaded', function(){ $(document.body).removeClassName('incompatible'); });
    }
  }
};

var Cookie = {
  get: function( name ){
    var nameEQ = escape(name) + "=", ca = document.cookie.split(';');
    for (var i = 0, c; i < ca.length; i++) {
      c = ca[i];
      while (c.charAt(0) == ' ') c = c.substring(1, c.length);
      if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
  },
  set: function( name, value, options ){
    options = (options || {});
    if ( options.expiresInOneYear ){
      var today = new Date();
      today.setFullYear(today.getFullYear()+1, today.getMonth, today.getDay());
      options.expires = today;
    }
    var curCookie = escape(name) + "=" + escape(value) + 
      ((options.expires) ? "; expires=" + options.expires.toGMTString() : "") + 
      ((options.path)    ? "; path="    + options.path : "") + 
      ((options.domain)  ? "; domain="  + options.domain : "") + 
      ((options.secure)  ? "; secure" : "");
    document.cookie = curCookie;
  },
  removeCookie: function (key) {
    var date = new Date();
    date.setTime(date.getTime()-(1*24*60*60*1000));
    this.set(key, '', {expires: date});
  },
  hasCookie: function( name ){
    return document.cookie.indexOf( escape(name) ) > -1;
  },
  checkSupport: function(){
    return this.hasCookie('compatibility_test');
  }
};

// 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){ /*TODO: should we add another log here? (console pulled out)*/ }
    });
  };

  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, options){
    options = options || {};
    element.morph('height:' + (options['height'] || '20px'), { 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;
    }
  },
  toHTML: function(element) { 
    // Taken from prototype mailing list
    // This is so you can take an element and pass it somewhere in string form.  
    
    if (typeof element=='string') element = $(element);  // IE needs that check with XML 
    return Try.these( 
      function() { 
        var xmlSerializer = new XMLSerializer(); 
        return  element.nodeType == 4 ? element.nodeValue : 
  xmlSerializer.serializeToString(element); 
      }, 
      function() { 
        return element.xml || element.outerHTML || $ 
  (element).clone().wrap().up().innerHTML; 
      } 
    ) || ''; 
  }
});

// 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;
});

String.prototype.titleize = function() {
	res = new Array();
	var parts = this.replace(/\-|_/g, ' ').split(' ');
	parts.each(function(part) {
		res.push(part.capitalize());
	});
	return res.join(" ");
};

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 || {});
    
    // Do remote sorting for ie 6/7 if required
    this.isRemote = this.table.hasClassName('remote') && (Browser.ie6 || Browser.ie7);

    this.sort_columns = $A(this.thead.getElementsByTagName('td')).collect(function(elem, index) {
      elem.sort_index = index;
      // elem.ascending  = elem.className.include('sorted');
      
      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));
    }


  },

  // 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));
      }
    }
  },
  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).hasClassName('not-clickable')) return;
    if (!tr || !element) return;
    if (opt.clickHandler && !$w('INPUT A TBODY').include(element.tagName.toUpperCase())) opt.clickHandler(tr);
  },
  
  
  // Called when someone actually clicks on a column header
  sort_column:function(event) {
    var col = event.element();
    
    if (this.isRemote) {
      this.remoteSort(col);
    } else {    
      this.setSortDirection(col);
      this.do_sort(col);
    }
  },
  
  remoteSort: function (element) {
    var column = element.getAttribute('column_name');
    var direction = element.hasClassName('asc') ? 'desc' : 'asc';
    var loc = document.location.href;
    
    loc = loc.gsub(/sort_by\=\w+/, 'sort_by=' + column);
    if (loc.indexOf('sort_by=') == -1){
      if(loc.indexOf('?') == -1){
        loc = loc + '?sort_by=' + column;
      }else{
        loc = loc + '&sort_by=' + column;        
      }
    }
    
    loc = loc.gsub(/sort_by_direction\=\w+/, 'sort_by_direction=' + direction);
    if (loc.indexOf('sort_by_direction=') == -1) {
      loc = loc + '&sort_by_direction=' + direction;
    }
    
    document.location = loc;
  },
  
  
  // 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;
    
    if (this.onSortColumnChange) {
      this.onSortColumnChange(this.current_sort_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;
      }
    }
    
    
    if (this.onSortDirectionChange) {
      this.onSortDirectionChange(ascending ? "asc" : "desc");
    }
    
    // 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);
    }
    
    $(this.tbody).select('.sort-bottom').each( function (row) {
        this.tbody.insert({bottom:row});      
    }.bind(this));
  },
  
  destroy: function(){
    $A(this.thead.getElementsByTagName('td')).each(function(elem, index) {
      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));
      }
    }.bind(this));

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


    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 SPICEWORKS.utils.sortFunctions.alphabetic(element);
    },
    versionSort: function(element) {
      return SPICEWORKS.utils.sortFunctions.version(element);
    },
    full_name: function(element) {
      return SPICEWORKS.utils.sortFunctions.fullName(element);
    },
    bytes: function(element) {
      return SPICEWORKS.utils.sortFunctions.bytes(element);
    },
    numeric: function(element) {
      return SPICEWORKS.utils.sortFunctions.numeric(element);      
    },
    date: function(element) {
      return SPICEWORKS.utils.sortFunctions.date(element);
    },
    ticket_priority: function(element){
      return SPICEWORKS.utils.sortFunctions.ticketPriority(element);
    },
    // This is the default sort order.  status/priority/id
    ticket_externally_updated: function(element){
      return SPICEWORKS.utils.sortFunctions.ticketExternallyUpdated(element);
    },
    ip_address: function(element) {
      return SPICEWORKS.utils.sortFunctions.ipAddress(element);
    },
    click_to_edit: function(element){
      return SPICEWORKS.utils.sortFunctions.clickToEdit(element);
    }
  },
  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._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');
  },

  _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);
      
      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');

    this.listeners = {
      rowRemovedFromDOM: this.rowRemovedFromDOMCallback.bindAsEventListener(this)
    };

    var rows = this.table.select('tbody tr');
    rows.each(this.prepareRow.bind(this).curry(rows));
    
    SPICEWORKS.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};

    if (!options.newRow){
      var controls = this._moveControlsForRow(moveableRow);
      if (moveableRow.down('span.position')) moveableRow.down('span.position').update(index+1);
      this._switchMoveUpDown(controls.moveUp, 'up', index != 0);
      this._switchMoveUpDown(controls.moveDown, 'down', index != allRows.size() - 1);
    }
  },
  moveUp: function(rowToMove, options){
    options = options || {};
    
    var rowToInsertBefore = rowToMove.previous('tr');
    if (!rowToInsertBefore) return;
    var newRowPosition = rowToMove.parentNode.insertBefore(rowToMove, rowToInsertBefore);
    
    SPICEWORKS.fire('reorderable-table:row-moved', this);

    if (options.ajaxCallback) new Ajax.Request(options.ajaxCallback);

    this.checkUpDown();
  },
  moveDown: function(rowToMove, options){
    options = options || {};

    var rowToInsertBefore = rowToMove.next('tr');
    if (!rowToInsertBefore) return; // last row cannot be moved down

    // this is a little funky, but we're trying to leverage native dom methods if possible
    // so as long as this isn't the second to the last row, we will always fetch two rows ahead and use insertBefore
    // but if this is the second to the last row, then we cannot use insertBefore, and we will therefore fall back on prototype's insertion method
    rowToInsertBefore = rowToInsertBefore.next('tr');
    if (rowToInsertBefore){
      // we are moving any row besides the second to the last row down one
      rowToMove.parentNode.insertBefore(rowToMove, rowToInsertBefore);
    } else {
      // we are trying to move the second to the last row into the last spot
      rowToMove.up('tbody').insert({bottom:rowToMove});
    }
    
    SPICEWORKS.fire('reorderable-table:row-moved', this);

    if (options.ajaxCallback) new Ajax.Request(options.ajaxCallback);
    
    this.checkUpDown();
  },
  destroy: function(){
    this.table.reorderable = null;
  },
  checkUpDown: function(){
    var rows = this.table.select('tbody tr'), controls;
    var that = this; // to avoid needing to bind
    rows.each(function(moveableRow, index){
      if (moveableRow.down('span.position')) moveableRow.down('span.position').update(index+1);

      if (!that.table.hasClassName('no-striping')) moveableRow.removeClassName('stripe1').removeClassName('stripe0').addClassName(index % 2 == 0 ? 'stripe1' : 'stripe0');

      controls = that._moveControlsForRow(moveableRow);
      that._switchMoveUpDown(controls.moveUp, 'up', index != 0);
      that._switchMoveUpDown(controls.moveDown, 'down', index != rows.size() - 1);
      
    });
  },
  rowRemovedFromDOMCallback: function(row){
    // this is called after the row is removed from dom
    this.checkUpDown();
  },
  _switchMoveUpDown: function(element, upOrDown, enableControl){
    if (!element) return;
    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;
    }
  },
  _moveControlsForRow: function(row){
    var menu = $('actions_' + row.id);
    if (!menu) return {};
    return {
      moveUp: menu.down('a.table-action-arrow-up'),
      moveDown: menu.down('a.table-action-arrow-down')
    };
  }
});

ReorderableTable.initialize = function(){
  var that = this;
  if (!this.tables) this.tables = $H();
  $$('table.reorderable').each(function(table){
    // don't instantiate tables that have already been added
    if (!that.tables.get(table.id)) that.tables.set(table.id, 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) {
    SPICEWORKS.observe('table-row:added', this.rowAdded.bindAsEventListener(this));
    this.observersSetup = true;
  }
};
ReorderableTable.rowAdded = function(event){
  var row = $(event.memo), table = row.up('table'), reference = ReorderableTable.find(table);
  if (reference) reference.rowAdded(row);
};
ReorderableTable.find = function(reference){
  if (typeof reference == 'string') reference = $(reference);
  if (!reference.hasClassName('reorderable')) reference = reference.up('table');
  return (reference ? this.tables.get(reference.id) : null);
};
ReorderableTable.moveUp = function(row, options){
  row = $(row);
  this.find(row.up('table')).moveUp(row, options);
};
ReorderableTable.moveDown = function(row, options){
  row = $(row);
  this.find(row.up('table')).moveDown(row, options);
};

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 options = this.table.getAttribute('editable_options') || '{}';
    this.options = Object.extend({
      deleteAllRows:true,
      newURL:'new'
    }, options.evalJSON());

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

    if (!this.table.hasClassName('no-add-new')){
      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(row, editURL){
    row = $(row);
    var 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});
    row.addClassName('editing');
    this.tbody.addClassName('editing');
    this.tfoot.addClassName('editing');
    SPICEWORKS.fire('editable-table:start-edit', row);
  },
  saveEdit: function(event){
    var row = (event ? event.findElement('tr') : this.table.down('tr.editing'));
    SPICEWORKS.fire('editable-table:save-edit', row);
  },
  cancelEdit: function(event){
    var row = (event ? event.findElement('tr') : this.table.down('tr.editing'));
    this._returnRowToNormal(row);
    SPICEWORKS.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()}});
  },
  rowAdded: function(){
    this.restripe();
    this._returnAddNewRowToNormal();
  },
  newRowShown: function(){
    SPICEWORKS.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();
    SPICEWORKS.fire('table-row:removed-from-dom', row);
    this.restripe();
  },
  destroy: function(){
    this.table.editable = null;
  },
  restripe: function(){
    var rows = this.tbody.select('tr');
    rows.each(function(row, index){
      if (row.down('span.position')) row.down('span.position').update(index+1);
      if (this.table.hasClassName('no-striping')) return;
      row.removeClassName('stripe1').removeClassName('stripe0').addClassName(index % 2 == 0 ? 'stripe1' : 'stripe0');
    }.bind(this));
  },
  
  _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(){
    if (!this.addNewForm) return;
    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 = $H();
  $$('table.editable').each(function(table){
    // don't instantiate tables that have already been added
    if (!that.tables.get(table.id)) that.tables.set(table.id, 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) {
    SPICEWORKS.observe('table-row:added', this.rowAdded.bindAsEventListener(this));
    SPICEWORKS.observe('table-row:removed', this.rowRemoved.bindAsEventListener(this));
    this.observersSetup = true;
  }
};
EditableTable.rowAdded = function(event){
  var row = $(event.memo), table = row.up('table'), reference = EditableTable.find(table);
  if (reference) reference.rowAdded();
};
EditableTable.rowRemoved = function(event){
  var row = $(event.memo);
  if (!row) return; // just in case this event is fired multiple times
  var table = row.up('table'), reference = EditableTable.find(table);
  if (reference) reference.removeRowCallback(row);
};
EditableTable.find = function(reference){
  var reference = $(reference);
  if (!reference.hasClassName('editable')) reference = reference.up('table');
  return (reference ? this.tables.get(reference.id) : null);
};
EditableTable.cancelNew = function(reference){
  this.find(reference).cancelNew();
  return false;
};
EditableTable.canDelete = function(reference, confirmation){
  return this.find(reference).canDelete(confirmation);
};
EditableTable.editRow = function(reference, editURL){
  this.find(reference).editRow(reference, editURL);
  return false;
};
EditableTable.saveEdit = function(reference){
  this.find(reference).saveEdit();
  return false;
};
EditableTable.stopEdit = function(reference){
  this.find(reference).cancelEdit();
  return false;
};
Event.register(EditableTable);

// all calls to this need to be updated to call spiceworks.js instead
var DynamicScriptInclude = { 
  load: function(source, nocache){
    var callback = (nocache ? Prototype.emptyFunction : null);
    SPICEWORKS.utils.include(source, callback);
  }
};

// this should be moved to spiceworks.js and all calls to this should be updated
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); 
  } 
};
// 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.parameters);
      
      var encodedToken = encodeURIComponent(Application.authenticityToken);
      var regex = new RegExp(encodedToken);
      if (!regex.match(request.options.postBody))
        request.options.postBody += "&authenticity_token=" + encodedToken;
    }
  },
  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()); }
  );
};

(function(){
  document.observe('dom:loaded', function(){
    // this datagrid implementation is very simple and leaves all the heavy lifting to you
    $$('div.datagrid').each(function(grid){
      // a means to fetch/persist datagrid preferences for the user
      function persistPref(pref, value){ Cookie.set(prefID(pref), value, {expiresInOneYear:true}); };
      function retrievePref(pref, value){ return Cookie.get(prefID(pref)); };
      function prefID(pref){ return grid.id + '_pref_' + pref; }
      
      if (grid.hasClassName('sortable-datagrid')){
        // if it is sortable, add the event listener on each header cell
        grid.select('thead th').each(function(headerCell){
          if (!headerCell.hasClassName('no-sort')) headerCell.observe('click', function(event){
            var cell = event.element();
            if (cell.up('th')) { cell = cell.up('th'); }
            if (cell.hasClassName('sorted')){
              if (cell.hasClassName('sorted-asc')) cell.removeClassName('sorted-asc').addClassName('sorted-desc');
              else cell.removeClassName('sorted-desc').addClassName('sorted-asc');
            } else {
              var previousSort = cell.up('tr').down('th.sorted');
              if (previousSort) previousSort.removeClassName('sorted').removeClassName('sorted-asc').removeClassName('sorted-desc');
              cell.addClassName('sorted sorted-asc');
            }
            
            var sortDirection = (cell.hasClassName('sorted-asc') ? 'asc' : 'desc'), sortBy = cell.className.match(/cell-([^\s]*)/)[1];
            // leave it up to the implementer to actually refresh the data or whatever
            SPICEWORKS.fire('datagrid:sorted:' + grid.id, {sortBy:sortBy, sortDirection:sortDirection});
          });
        });
      }
      if (grid.hasClassName('clickable-datagrid')){
        // if it is clickable, add the event listener on the body (so we don't have to keep tabs on row listeners) and fire an event when anything is clicked
        grid.down('div.table-body-wrapper').observe('click', function(event){
          var clicked = event.element();
          if (!clicked) return;
          // ignore clicks on links and input boxes (checkbox) as those events should not trigger a row click
          if (clicked.tagName == 'A' || clicked.tagName == 'INPUT') return;
          if (clicked.tagName != 'TR') clicked = clicked.up('tr');
          if (!clicked) return;
          
          if (clicked.hasClassName('clicked')){
            // clicked row clicked again, do something if the configuration of this table wants us to
            if (grid.hasClassName('unclickable-datagrid')){
              clicked.removeClassName('clicked');
              SPICEWORKS.fire('datagrid:row-unclicked:' + grid.id, clicked);
            }
          } else {
            grid.select('tr.clicked').invoke('removeClassName', 'clicked');
            clicked.addClassName('clicked');
            SPICEWORKS.fire('datagrid:row-clicked:' + grid.id, clicked);
          }
        });
      }
    });
  });
  
  function finalizeUpdates(options){
    // the datagrid was updated by the server and now the table needs to be renewed in some way
  };
  window.datagrid = {
    finalizeUpdates:finalizeUpdates
  };
})();

(function(){
  var resizable = {
    create: function(resizable){
      function persistPref(pref, value){ Cookie.set(prefID(pref), value, {expiresInOneYear:true}); };
      function retrievePref(pref, value){ return Cookie.get(prefID(pref)); };
      function prefID(pref){
        var identifier = resizable.className.match(/rz-id-([^\s]*)/);
        if (identifier) identifier = identifier[1];
        else identifier = resizable.id;
        return identifier + '_pref_' + pref;
      }

      if (!resizable.id) resizable.identify();
      var wrapper = resizable.wrap(new Element('div', {'class':'resizable-vertical-wrapper', 'id':'wrapper-' + resizable.id}));
      wrapper.insert(new Element('div', {'class':'resize-vertical-control'}));
      var baseHeight = resizable.getHeight(), heightPref = retrievePref('base-height');

      if (heightPref){
        baseHeight = parseInt(heightPref, 10); // need to make sure we have an integer otherwise bad things will happen when we do arithmetic later
        resizable.setStyle({height:baseHeight+'px'});
      } else if (resizable.className.match(/default-max-\d+/)) {
        var defaultMax = parseInt(resizable.className.match(/default-max-(\d+)/)[1], 10);
        if (baseHeight > defaultMax) {
          resizable.setStyle({height:defaultMax+'px'});
          baseHeight = defaultMax;
        }
      }
      new Draggable(wrapper, {
        handle: 'resize-vertical-control',
        starteffect: function (){ if (Prototype.Browser.Gecko) resizable.setStyle({overflow:'hidden', 'overflow-y': 'hidden'}); }, // hack for Firefox, see #15564
        endeffect: function (){
          if (Prototype.Browser.Gecko) resizable.setStyle({'overflow': 'auto', 'overflow-x': 'hidden', 'overflow-y':'scroll'}); // unhack for Firefox, see #15564
          
          // update the baseHeight otherwise the box will jump back to the original (page load) height on a later drag event
          baseHeight = resizable.getHeight();
          // save the height to a cookie so we can auto-load it next time
          persistPref('base-height', baseHeight);
        },
        constraint: 'vertical',
        change: function (draggable) {
          // the new height is the old height plus whatever height we have scrolled for this drag session
          var heightOffset = draggable.currentDelta()[1], newHeight = baseHeight + (heightOffset);

          wrapper.setStyle({top:'0'}); // keep the box's top edge fixed, we just want to increase the height, note that this has to come after the drag delta is calculated

          // set the new height and then when the drag is done we will update the baseHeight
          resizable.setStyle({height:newHeight+'px'});
        }
      });
    }
  };
  
  document.observe('dom:loaded', function(){
    $$('.resizable-vertical').each(resizable.create);
  });
  
  window.resizable = resizable;
})();
