/*

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;
  Event.observe(document, 'dom:loaded', function(){
    // for each item in the stack, call the initialize method
    Event.registeredEvents.each(function(object){
      object.initialize();
    });
  });
  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) : '');
  }
});

// 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) {
    sorted_column = sorted_column || this.current_sort_col;
    // 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;
      Element.extend(cell).removeClassName('sorted').removeClassName('asc').removeClassName('desc');
    }

    if (this.current_sort_col && this.current_sort_col !== sorted_column) {
      // don't toggle the sort direction -- just set the new column
      sorted_column.addClassName("sorted " + (sorted_column.ascending ? "asc" : "desc"));
    } else {
      sorted_column.addClassName("sorted " + (sorted_column.ascending ? "desc" : "asc"));
      sorted_column.ascending = !sorted_column.ascending;
    }
  },

  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(){ 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(){
    this.tables = $H();
    this._attachNew();
  },
  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 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 { // DOM
      styleNode.update(cssText);
    }
    $$('head').first().appendChild(styleNode);
  }
};


/*
 * A simple mixin that allows you to register listeners on the extended object.
 * usage:
 *   var MyObject = { ... }
 *   Object.extend(MyObject, ObserverMixin);
 *   MyObject.observerEvent('foo', function(evt){ ... });
 *   ...
 *   MyObject.fireEvent('foo');
 */
var ObserverMixin = {
  observeEvent: function(event, func){
    // console.log('observing ' + event);
    if(! this.observers) this.observers = $H();
    if(! this.observers.get(event)) this.observers.set(event, $A([]));
    this.observers.get(event).push(func);
  },
  fireEvent:function(event){
    // console.log('firing ' + event);
    if(this.observers){
      this.observers.get(event).each(function(func){
        func(event);
      });
    }
  }
};


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

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