// Copyright © 2006-10 Spiceworks Inc. All Rights Reserved. http://www.spiceworks.com

var Dispatcher = {
  listeners: {},
  initialize: function() {
    document.observe("click", this.click.bindAsEventListener(this));
  },
  register: function(action, callback) {
    if (!this.listeners[action]) {
      this.listeners[action] = [];
    }
    this.listeners[action].push(callback);
  },
  click: function(event) {
    this.listeners["click"].each(function(callback) {
      try {
        // Every one of these needs to get called
        callback(event);
      }
      catch(err) {
        // 
      }
    });
  }
};
Event.register(Dispatcher);

var Application = {
  runMode:'production',
  cobranded:false,
  uuid:null,
  authenticityToken:null,
  state: {},
  initialize: function(){
    if (Application.needsGoalTracking) Application.trackGoal(Application.needsGoalTracking);
  },
  narrowLayout: function(){
    $(document.body).addClassName('narrow');
    Application.narrow = true;
  },
  beginPolling: function( sourceController, sourceAction, options ){
    this._configureOptions( options );
    // keep track of where the user is within the application, so the receiving ajax can determine what extra actions it needs to perform, if any
    this.sourceController = sourceController;
    this.sourceAction = sourceAction;
    this._boundEndPolling = this.endPolling.bind(this);

    this._startPolling();
  },
  endPolling: function(){
    this._stopPolling();
  },
  slowPollingSpeed: function() {
    if (this.poller) {
      this.poller.frequency = 120;
    }
  },
  resumePollingSpeed: function() {
    if (this.poller && this.poller.frequency == 120) {
      this.beginPolling(this.sourceController, this.sourceAction, this.options);
    }
  },
  renewPolling: function( options ){
    this._configureOptions( options );
    if (this.options.scanRunning) this._scanStarted({delayedStart:false});
    else this._scanStopped({delayedStart:false});
  },
  updatePageUri: function(hardLink){
    var myHref = location.href.toString(), newUrl, matches = null;
    if (matches = myHref.match(/#(.+)/)){
      newUrl = myHref.replace(matches[0], hardLink);
    } else {
      newUrl = location.href + hardLink;
    }
    newUrl = newUrl.replace(/\#+/, '#');
    location.href = newUrl;
  },
  getUriAnchorParams: function(options) {
    var updateOptions = Object.extend({stripOrphanedKeys:true}, options || {});    
    var hashParams = $H();
    var myHref = location.href.toString(), matches = null, pairs;
    if (matches = myHref.match(/#(.+)/)){
      pairs = matches[1].toString().split('&');
      $A(pairs).each(function(pair) {
         pair.scan(/(\w+)\-(.+)/, function(w) { 
            hashParams.set(w[1], w[2]);   
         });
       });
    }
    return hashParams;
  },
  buildUriAnchorQueryString: function(params, options) {
    var stringOptions = Object.extend({separator:'-'}, options || {});
    var hashUrl = '';
    var paramArray = $A();
    params = $H(params);
    params.keys().sort().each(function(key) {
      paramArray.push(key + stringOptions.separator + params.get(key));
    });
    hashUrl = paramArray.compact().join('&');    
    hashUrl = "#" + hashUrl;
    return hashUrl;
  },
  updateAnchorParamSection: function(param, section) {
    // Used for when there are different selectable sections within a tab
    // i.e. hotfixes, services, and applications within the software tab
    // setting the section allows 
    
    var value = this.getUriAnchorParams().get(param) || "";
    var params = {};
    
    if (!param) { return; }
    
    if (section != "") {
      params[param] = value.split("/")[0] + "/" + section;
    }
    else {
      params[param] = value.split("/")[0];      
    }
    
    Application.updateUriAnchorParams(params, {merge: true});
  },
  updateUriAnchorParams: function(params, options) {  
    var updateOptions = Object.extend({merge:true, compact: true}, options || {});
    
    var hashParams, myHref = location.href.toString(), matches = null, hashUrl ="";    
    if (updateOptions.merge) hashParams = $H(this.getUriAnchorParams()).merge(params);
    else hashParams = $H(params);
    
    if (updateOptions.compact) hashParams = hashParams.compact();
    
    var newUrl = this.buildUriAnchorQueryString(hashParams);
    
    if (matches = myHref.match(/#(.+)/)){
      newUrl = myHref.replace(matches[0], newUrl);
    } else {
      newUrl = location.href + newUrl;
    }
    newUrl = newUrl.replace(/\#+/, '#');    

    // To prevent the page from jumping to the the top when
    newUrl = newUrl.replace(/\&?no\-jump/, "");
    newUrl = newUrl.replace(/\#$/, "#no-jump");
    
    if (location.href != newUrl) {
      location.href = newUrl;
    }
  },
  updateScanStatus: function(scanRunning){
    if (this.options.scanRunning && !scanRunning) this._scanStopped({delayedStart:false});
    else if (!this.options.scanRunning && scanRunning) this._scanStarted({delayedStart:false});
  },
  registerLogoRerenderFix: function(){
    if ( $$('h1').first().down('img') && $('content_wrapper') ){
      Event.observe( $$('h1').first().down('img'), 'load', function(){
        $('content_wrapper').forceRerendering();
      });
    };
  },
  inDevelopment: function() { return this.runMode == 'development'; },
  inProduction: function() { return this.runMode == 'production'; },
  
  goBackOrTo: function (url) {
    if(window.history.length == 1){
      document.location = url;
    }else{
      window.history.back();
    }
  },
  
  _scanStarted: function(options){
    SPICEWORKS.fire('app:scan:started');
    this.options.scanRunning = true;
    document.body.addClassName('scan-running');

    // renew the poller, since when the scan is running the polling options are different
    this._startPolling(options);
  },
  _scanStopped: function(options){
    SPICEWORKS.fire('app:scan:stopped');
    this.options.scanRunning = false;
    document.body.removeClassName('scan-running');

    // renew the poller, since when the scan is NOT running the polling options are different
    this._startPolling(options);
  },
  _startPolling: function(startOptions){
    startOptions = Object.extend({delayedStart:true, decay:1.5, pollingFrequency:10}, startOptions || {});
    this.options.pollingFrequency = ((this.sourceController == 'network' || this.options.scanRunning) ? 5 : 10);
    this._stopPolling(); // make sure the poller is not already running
    this.poller = new Ajax.PeriodicalUpdater('', '/finder/application_polling',  { decay: startOptions.decay, maxDecay:this.options.maxDecay, frequency: this.options.pollingFrequency, maxFrequency: 120, delayedStart: (startOptions.delayedStart ? this.options.pollingFrequency : false), parameters: { source_controller: this.sourceController, source_action: this.sourceAction, requestClass: 'application_polling' } } );
  },
  _stopPolling: function(){
    if ( this.poller && this.poller.stop ) this.poller.stop();
    this.poller = null;
  },
  _configureOptions: function( options ){
    this.options = Object.extend({
      pollingFrequency:10,
      maxDecay:3,
      scanRunning:false
    }, options || {} );
  },
  // Simple refresh, just reset the iframe src to the same thing (will load another random ad with the same context)
  // Add &jsr=1 if needed (javascript refresh!)
  refreshAd: function (ctx) {
    ctx = ctx || {};
    if (typeof ctx == 'string') {
      ctx = {'jsr': ctx};
    }
    
    if ($('adframe')) {
      var src = $('adframe').src.toString();
      
      Object.keys(ctx).each(function (key) {
        var re = new RegExp(key + '\\=[^\&]*');
        if (!src.match(re)) {
          src = src + "&" + key + "=" + ctx[key];
        }else{
          src = src.gsub(re, key + "=" + ctx[key]);
        }
      });
      
      $('adframe').src = src;
    }
  },
  
  updateState: function(state, urlOptions) {
    var anchorParams;    

    /* We should migrate the global state information from Toolbar to here */
    this.state = Object.extend(this.state || {}, state || {});
    urlOptions = Object.extend({updateUri: true, merge: true}, urlOptions || {});    
  
    if (urlOptions.updateUri) {
      Application.updateUriAnchorParams(state, {merge: urlOptions.merge});      
    }  
  },
  
  // can invoke arbitrary URLs on www for the purposes of getting app-driven actions into google analytics
  trackGoal: function(target){ $(document.body).insert('<iframe src="http://www.spiceworks.com/app-launch/' + target + '.php" frameborder="0" border="0" scrolling="no" style="position:absolute;right:5px;bottom:5px;width:3px;height:3px;opacity:0;-moz-opacity:0;filter:alpha(opacity=0);" />'); }
};

Event.register(Application);
Event.observe(window, 'focus', Application.resumePollingSpeed.bind(Application));
Event.observe(window, 'blur', Application.slowPollingSpeed.bind(Application));

var Finder = {
  startLocalhostPoller: function(){
    if (this.localhostPoller) this.localhostPoller.stop();
    this.localhostPoller = new Ajax.PeriodicalUpdater('', '/finder/status_of_shallow_scan', { frequency:5 });
  },
  stopLocalhostPoller: function(){
    if (this.localhostPoller) this.localhostPoller.stop();
  }
};

var ClusterView = {
  groupMenus: $H(),
  initialize: function() {
    this.groupContainer = $('agg_wrap');
    $("agg_wrap").select("li span.handle").invoke("observe", "click", this.startReordering.bindAsEventListener(this));
    SPICEWORKS.observe('app:helpdesk:ticket:opened', this.indicatorChanged.bindAsEventListener(this, 'ticket', 1));
    SPICEWORKS.observe('app:helpdesk:ticket:closed', this.indicatorChanged.bindAsEventListener(this, 'ticket', -1));
    SPICEWORKS.observe('app:alert:created', this.indicatorChanged.bindAsEventListener(this, 'alert', 1));
    SPICEWORKS.observe('app:alert:cleared', this.indicatorChanged.bindAsEventListener(this, 'alert', -1));
    SPICEWORKS.observe("app:scan:error:cleared", this.indicatorChanged.bindAsEventListener(this, "error", -1));
  },
  indicatorChanged: function(event, type, increment) {
    var memo = event.memo;
    var indicator, newCount, li;
    if ((memo.assetType == "Device") && (memo.assetId) && (memo.assetGroups)) {
      memo.assetGroups.each(function(cat) { 
        li = $("category_" + cat);
        indicator = li.down("span.count_wrap span.count_" + type + " em");
        newCount = (parseInt(indicator.innerHTML, 10) || 0) + increment;
        if (newCount < 0) { newCount = 0; }
        indicator.update(newCount);
        indicator[(newCount == 0 ? 'hide' : 'show')]();
      });
    }
  },
  resorted: function(sortable){
    // post new order back to server
    var params = Sortable.serialize(sortable);
    new Ajax.Request('/settings/categories/resort', { parameters:params });
  },  
  startReordering: function(event) {
    event.stop();
    if (this.reorderingMode) return false;  
    this.reorderingMode = true;
    
    this._prepareSortable();
    $('primary').select("span.agg_wrap_message").invoke('blindDown', '{duration:0.3}');
    this._growDraggingArea();
    
    $('agg_wrap').select('li').invoke("addClassName", "draggable");
    $('agg_wrap').select("li a span.handle").invoke("writeAttribute", "title", "Drag to reorder");
  },
  endReordering: function() {    
    this.reorderingMode = false;
    $('primary').select("span.agg_wrap_message").invoke('blindUp', '{duration:0.3}');
    $('agg_wrap').select('li').invoke("removeClassName", "draggable");
    this._shrinkDraggingArea();
    $('agg_wrap').select("li a span.handle").invoke("writeAttribute", "title", "Click to arrange icons");
    Sortable.destroy(this.groupContainer);
  },
  _growDraggingArea: function() {
     var scrollHeight = $('primary').scrollHeight + 50; //take into account the top and bottom messages
     var scrollTop = $('primary').scrollTop;
     var styleOptions;
     
     if (Browser.ie6) { styleOptions = 'height:' + scrollHeight + 'px'; }
     else { styleOptions = 'max-height:' + scrollHeight + 'px'; }

     var scrollWindowEffect = new Effect.ScrollToPosition(0,scrollTop);  
     var scrollAggWrapEffect = new Effect.Morph('primary', {style: styleOptions, afterFinishInternal: function() { document.fire("dom:updated"); }});

     if (scrollTop > 0) new Effect.Parallel([scrollWindowEffect, scrollAggWrapEffect], {duration:0.5});
     else new Effect.Parallel([scrollAggWrapEffect], {duration:0.5});
  },
  _shrinkDraggingArea: function () {
    if (Browser.ie6)  $('primary').setStyle('height:277px;', {duration:0.5});
    else $('primary').morph('max-height:274px', {duration:0.5, afterFinishInternal: function() { document.fire("dom:updated"); }});
  },
  startDrag: function() {
    Draggables.activeDraggable.element.addClassName('dragging');
  },
  endDrag: function() {
    Draggables.activeDraggable.element.removeClassName('dragging');
  },
  _prepareSortable: function(){ 
    Sortable.create(this.groupContainer, {constraint: false, overlap:'horizontal', onUpdate:this.resorted.bind(this)});
    Draggables.addObserver({
       onStart: function() {ClusterView.startDrag(); }, 
       onEnd: function() { ClusterView.endDrag(); }
     });
  },
  groupMouseout:function(icon) {},
  groupMouseover:function(icon) {  
    this.hideOtherMenus(icon);
  },
  hideOtherMenus: function(icon) {
    var thisMenu = $(icon.id).down('span.group_menu');
    TipManager.tipsWithTag("group_menu").pluck('target').without(thisMenu).pluck('prototip').invoke('hide');
  },
  counterClick:function(icon, category){
    var href = $(icon).up('a').href;
    var quickfind = icon.getAttribute('quickfind');
    href += [ 'software', 'installables', 'installable' ].include( category ) ? '?q=@' + quickfind + '@' : '/@' + quickfind + '@';
    location.href = href;
  },
  deleteGroup: function(catID) {
      TipManager.hideAll();
      new Ajax.Request('/settings/categories/destroy/' + catID);
      $('category_' + catID).fade();
  },
  editRules: function(catID) {
    var href = "/settings/categories?edit_category=" + catID;
    location.href = href;
  },
  editName: function(catID) {
      var view =$("category_" + catID).down('a.view');
      var edit =$("category_" + catID).down('div.edit');
            
      TipManager.newTip(edit.down('span.change_icon'), {
        ajax: {
          url: '/settings/categories/icon_chooser/' + catID + '?from_page=front',
          options: {
            onComplete: function() { }
          }
        },
        target: edit.down('img'),
        title: "Click on the icon you'd like to use for this Group",
        stem: {position: "topMiddle", height: 12 ,width: 20},
        hook: {tip:'topMiddle', target: 'bottomMiddle', mouse: false},
        showOn: 'click',
        offset: {x: 0, y: 0},
        forceRedraw: true,
        tags: ['icon_chooser'],
        width:400,
        style: 'workflow'
      });
      
      var menu = view.down('span.group_menu');   
      if (menu.prototip) { menu.prototip.hide(); }
      view.hide();
      edit.show();
  },  
  endEdit: function(catID) {
    var view =$("category_" + catID).down('a.view');
    var edit =$("category_" + catID).down('div.edit');
    
    view.show();
    edit.hide();
  },
  iconChosen: function(iconName, options){
    options = Object.extend({id:null}, options || {});
    
    var newSource = '/images/icons/large/' + iconName + '.png';
    $("category_" + options.id).down('div.edit img').src = newSource;
    $("category_" + options.id).down("div.edit #new_icon_" + options.id).value = iconName;
    TipManager.tipsWithTag('icon_chooser').invoke('hide');
  },
  deleteConfirmation: function(catID, catName) {
    if (confirm("Are you sure you want to delete the group, '" + catName + "'?   This action can not be undone, but will not delete the devices in the group."))  {
     this.deleteGroup(catID);
    }
  },
  hideConfirmation: function(catID, catName) {
    if (confirm("Are you sure you want to delete the group, '" + catName + "'?  You can undo this in the custom groups settings.")) {
      this.deleteGroup(catID);
    }
  }
};

var Messaging = {
  PREFIX: 'application_messaging_',
  initialize: function(){
    if ( !this.initialized ){
      this.initialized = true;
      this.expiredMessages = $A();
      this.container = $( 'application_messaging' );
      if (!this.container) return;
      this.visible = this.container.visible();
      if ( !this.container ) return;

      this.list = new Element( 'ol' );
      this.container.appendChild(this.list);
      this.list = this.container.down('ol');
    }
  },
  push: function( messageID, messageBody, options ){
    if ( !this.initialized ) this.initialize();
    
    var globalID = this.PREFIX + messageID; // create our element ID, should be unique
    if ( $(globalID) ) return; // if the element is already on the page, don't do anything

    options = Object.extend({
      dismissable:false, // give the message a clickable element to remove it
      informative:false, // tooltip and icon for more information on message
      ajaxOnDismiss:true, // fire an AJAX call when the dismiss button is clicked
      selfRemoving:false, // make the message remove itself after timeSeconds have lapsed
      timeoutSeconds:5 // when selfRemoving is true, this is time duration the message is displayed
    }, options || {});
    
    var message = new Element( 'li', { id: globalID, message_id: messageID }).update( messageBody );
    
    if ( options.informative ) this._makeInformative(message, messageID, options.tipOptions);
    if ( options.dismissable ) this._makeDismissable(message, messageID, options.ajaxOnDismiss);

    this._hideAll();
    this.list.appendChild( message );

    this.container.appear({duration:0.50});
    this.visible = true;

    if ( options.selfRemoving ) this.pop.bind(this, messageID).delay(options.timeoutSeconds);
    
    return message;
  },
  pop: function( messageID ){
    var message;
    if ( messageID ) message = this._removeByID( messageID );
    else message = this._removeLast();
    
    if (message) {
      var infoBox = message.down('a.info');
      if (infoBox) infoBox.prototip.remove();      
    }
    
    this._hideAll();
    var last = this.messages().last();
    if (last) last.show();
    else this._noMessages();
    
    return message;
  },
  
  dismissMessage: function(event, messageID, ajaxOnDismiss){
    if (event) event.stop();
    this.pop(messageID);
    if (ajaxOnDismiss) new Ajax.Request('/utility/hide_trait/', { parameters: { id: messageID } } );
  },

  messages: function(){ return this.list.select( 'li' ); },
  
  _noMessages: function(){
    var self = this;
    setTimeout(function () {
      self.container.hide();
      self.visible = false;
    }, 500);
  },
  _makeDismissable: function( message, messageID, ajaxOnDismiss ){
    message.addClassName( 'dismissable' );
    var dismisser = new Element('a', { 'class': 'dismisser', title: 'Dismiss this message' } ).update('<img src="/images/icons/square_close_white.png" alt="dismiss" width="10" height="10" />');
    dismisser.observe('click', this.dismissMessage.bindAsEventListener(this, messageID, ajaxOnDismiss));
    message.appendChild(document.createTextNode(' '));
    message.appendChild(dismisser);
  },
  _makeInformative: function( message, messageID, tipOptions ){
    var info = new Element('a', { 'class': 'info' } ).update('<img src="/images/icons/indicators/info.png" alt="" width="10" height="10" />');
    message.appendChild(document.createTextNode(' '));
    message.appendChild(info);
    
    TipManager.newTip(info, tipOptions);
  },
  _removeByID: function( messageID ){
    return this._remove( this.list.down( '#' + this.PREFIX + messageID ) );
  },
  _removeLast: function(){
    return this._remove( this.messages().last() );
  },
  _remove: function( message ){
    if ( message ) message.parentNode.removeChild( message );
    return message;
  },

  _hideAll: function(){ this.messages().invoke( 'hide' ); }
};

Event.register(Messaging);


/*
 * Helper methods for dealing with Dismissable boxes.
 */
 var DismissableInfoBox = {

   dismiss:function(id){
     if($(id)){
       new Effect.BlindUp(id);
       text = $(id).down('h4').text();
       Messaging.push( id + '_info_box_removed',"Removed '"+ text.truncate(30) + 
       "' (<a href='#' onclick='DismissableInfoBox.undo(\"" + id + "\");return false;'>undo</a>)", {selfRemoving:true, timeoutSeconds:10} );
     }
     new Ajax.Request( '/help/hide_tip/' + id );
   },

   undo:function(id){
     if($(id)){
       new Effect.BlindDown(id);
       Messaging.pop( id + '_info_box_removed' );
       new Ajax.Request( '/help/show_tip/' + id );
     }
   }
 };

var Community = {
  go: function( uri , new_window){
    var cform = $('community');
    if ( uri ) cform.community_redirect.value = uri;
    else cform.community_redirect.value = '';
 
    if (new_window == true) cform.target = '_blank';
    else cform.target = '';

    // for tracking, one url for the store, and the other for everything else
    var hit = ( ( uri && uri == '/store' ) ? '/store/hit' : '/community/hit' ) + '?redirect=' + uri;
    new Ajax.Request( hit, { method: 'get', onComplete: function(){ $('community').submit(); } } );

    return false;
  },
  uri: function( new_uri ){ 
    var community_form = $('community'); 
    if ( new_uri && community_form ){ 
      // set the uri 
      community_form.setAttribute('action', new_uri); 
    } 
    return ( community_form ? community_form.action : '' );
  }
};

var Navigation = Class.create({
  initialize: function( list ){
    this.list = $(list);
    if (this.list.getAttribute('related_list')) this.relatedList = $(this.list.getAttribute('related_list'));
    this.heading = this.list.down('dt');
    this.key = this.heading.innerHTML.gsub(' ', '-'); // we use this as a unique ID to correspond to this list
    this.items = this.list.select('dd');
    
    this.listeners = {
      headingClick: this.headingClick.bindAsEventListener(this)
    };
    
    if ( this.list.hasClassName('toggleable') ){
      this.heading.observe( 'click', this.listeners.headingClick );
      this._prepareState();
    }
  },
  headingClick: function(event){ this.toggle(); },
  toggle: function(){
    var isClosed = this.list.hasClassName('closed');
    this._setListMode( isClosed ); // we are flipping the state here
    this._saveState( isClosed ? 'open' : 'closed' );
  },
  visible: function(){ return !this.list.hasClassName('closed'); },
  _setListMode: function(isClosed){
    if (isClosed){
      this.list.removeClassName('closed');
      this.heading.setAttribute( 'title', 'Close this section' );
      if (this.relatedList) this.relatedList.removeClassName('closed');
    } else {
      this.list.addClassName('closed');
      this.heading.setAttribute( 'title', 'Open this section' );
      if (this.relatedList) this.relatedList.addClassName('closed');
    }
  },
  _prepareState: function(){
    var state = Cookie.get(this.key);
    if (this.list.down('dd.current')){
      // re-open closed section if it is the current one
      state = 'open';
      this._saveState('open');
    } 
    if (state) this._setListMode( state == 'open' );
  },
  _saveState: function(state){
    Cookie.set(this.key, state, { expiresInOneYear: true } );
  }
});

var NavigationManager = {
  initialize: function(sponsorshipURL){
    this.sponsorshipURL = sponsorshipURL;
    this.sponsorshipBox = $('sponsored_block');

    document.observe('dom:loaded', this._initializeSponsorshipBox.bindAsEventListener(this));

    this.lists = $H();
    $$( '#navigation dl' ).each( function( list, index ){
      this.lists.set( list.id || 'navigation_menu_' + index, new Navigation( list ) );
    }.bind( this ));
    
  },
  shouldAllowAddNewClick: function(){ return !this.showingNewForm; },
  toggleDelete: function(){
    if (this.editMode) this.toggleEdit();
    if (!this.deleteMode){
      this.lists.get('navigation_my_stuff').list.addClassName('deleteable');
      this.deleteMode = true;
    } else {
      this.lists.get('navigation_my_stuff').list.removeClassName('deleteable');
      this.deleteMode = false;
    }
  },
  toggleEdit: function(){
    if (this.deleteMode) this.toggleDelete();
    if (!this.editMode){
      this.lists.get('navigation_my_stuff').list.addClassName('editable').select('dd.editable').invoke('removeClassName', 'editing');
      this.editMode = true;
    } else {
      
      this.lists.get('navigation_my_stuff').list.select('form').invoke('remove');
      this.lists.get('navigation_my_stuff').list.removeClassName('editable');
      this.editMode = false;
    }
  },
  editingItem: function(item){
    var dd = $('custom_nav_' + item + '_wrapper');
    dd.addClassName('editing');
    dd.down('a.portal_link').blindUp({ duration:0.5 });
    this.toggleEdit();
  },
  cancelEdit: function(formToRemove){
    if (typeof formToRemove == 'string') formToRemove = $(formToRemove);
    formToRemove.blindUp({duration:0.5});
    this.lists.get('navigation_my_stuff').list.select('dd.editing a.portal_link').invoke('blindDown', { duration:0.5 });
    setTimeout( function () {
      this.toggleEdit();
      this.lists.get('navigation_my_stuff').list.select('dd.editing').invoke('removeClassName', 'editing');
    }.bind(this), 500);
  },
  newNavigationItemCreated: function(item){
  },
  newNavigationItemDestroyed: function(item){
    if (this.deleteMode) this.toggleDelete();
  },
  newNavigationFormShown:function(){
    if (this.editMode) this.toggleEdit();
    if (this.deleteMode) this.toggleDelete();
    this.showingNewForm = true;
    var list = this.lists.get('navigation_my_stuff');
    if (list && !list.visible()) list.toggle();
  },
  newNavigationFormHidden:function(){ this.showingNewForm = false; },
  
  sponsorshipRender: function(content){
    if (content != ''){
      this.sponsorshipBox.update(content);

      this.sponsorshipBox.select('a').each(function(anchor){
        if (!anchor.target) anchor.setAttribute('target', '_blank');
      });

      this.sponsorshipBox.up('#navigation_closeout').addClassName('sponsored');
      this.sponsorshipBox.show();
    }
  },
  _initializeSponsorshipBox: function(){
    var url = this.sponsorshipURL;
    url += ( url.include('?') ? '&' : '?');
    url += 'cobranded=' + Application.cobranded + '&uuid=' + escape(Application.uuid);
    SPICEWORKS.utils.include(url, function(){ /* give a callback so that this script will be removed after being loaded */ });
  }
};

var Store = { go: function() { Community.go( '/store' ); return false; } };

var Guides = {
  pane: null,
  height:'315px',
  top: '-30px',
  buttons: {},
  initialize: function(name, options) {
    options = Object.extend({}, options || {});
    this.guideKey = options.guideKey;
    this.guideSeen = options.guideSeen || false;
    this.useCSS3Effects = (Browser.features.csstransitions && Browser.features.csstransforms);
    
    var that = this;
    new Ajax.Request("/utility/guides/", {parameters: {id: name, requestClass: 'guides'},
      onSuccess: function(transport) {
        if (transport.responseText) {
          window.setTimeout(function() {
            that._setupObservers();
            that.initialized = true;
            that.show(name, options);
          }, 200).bind(that); 
        }
      }.bind(this)
    });
  },
  toggle: function(name, options) {    
    options = Object.extend({}, options || {});
    if (!this.initialized) {
      this.initialize(name, options);
      return;
    }

    this[$(document.body).hasClassName('guides-shown') ? "hide" : "show"](name, options);
  },
  show: function(name, options) {
    options = Object.extend({}, options || {});
    if (!this.initialized) {
      this.initialize(name, options);
      return;
    }
    var that = this;
        
    this._showLoadingScreen();
    if (this.useCSS3Effects) { 
      this.frame.show();
      window.setTimeout(function() {
        $(document.body).addClassName("guides-shown");         
      }, 100);
    }
    else {
      if (this.transitioning) { return; }
      this.disableToggleWhileTransitioning(1200);
      var that = this;
      
      $(document.body).addClassName("guides-shown");
      this.bottom.hide();
      
      if ($(document.body).hasClassName('fluid')) {
        this.frame.setStyle("top:" + this.top + "; left:-8px");
      }
      else {
        this.frame.setStyle("top:" + this.top + "; left:0");        
      }
      
      new Effect.Morph($("content"), { style: "padding-top:204px;!important", duration: 0.5});
      window.setTimeout(function() {
        new Effect.Appear(that.frame, {duration: 0.5});
        new Effect.Appear(that.pane, {duration: 0.5});
        new Effect.Appear(that.pane.down("div.content_column"), {duration: 0.5});
        new Effect.Grow(that.frame, { direction: 'bottom-right', duration: 0.5});
      }, 500);

      window.setTimeout(function() {
        new Effect.Appear(that.bottom, {duration: 0.25});
      }, 1000 );
    }

    this._updateNavigation();
    window.setTimeout(function() { 
      that.loadVideo();
    }, 1200);
    if (options.page) { window.setTimeout(function() { that.jumpTo(options.page); },1200); }
  },
  hide: function(name) {
    var that = this;
    
    if (!this.useCSS3Effects) {
      // we only need to lock the toggler for scriptaculous
      if (this.transitioning) { return; }
      this.disableToggleWhileTransitioning(1200);
    }
    
    if (!this.guideSeen) {
      new Ajax.Request("/utility/hide_trait", {parameters: {id: this.guideKey, user_specific: true}});
    }
    this.clearVideo();
    
    if (this.useCSS3Effects) {
      this._enableTransformsForWebKit();
      $(document.body).removeClassName("guides-shown");
      return;
    }
    else {
      var that = this;        
      new Effect.Fade(this.pane, {duration: 0.5});
      new Effect.Fade(this.bottom, {duration: 0.2});
      new Effect.Shrink(this.frame, { direction: 'bottom-right', duration: 0.5});
      window.setTimeout(function() {
        new Effect.Morph($("content"), { style: "padding-top:0px;!important", duration: 0.5});
      }, 500);
      
      $(document.body).removeClassName("guides-shown");  
    }
  },
  next: function(next) {
    if (!next) { next = this.pane.next(); }
    return (this._slidePanels(next, "left"));
  },
  previous: function(previous) {
    if (!previous) { previous = this.pane.previous(); }
    return (this._slidePanels(previous, "right"));
  },
  jumpTo: function(to) {
    this._clearHoverState();
    to = $(to + "-guide");
    
    var thisPage = this._getPageNumber(this.pane);
    var toPage = this._getPageNumber(to);
    
    if (thisPage > toPage) {
      this.previous(to);
    }
    else if (thisPage < toPage) {
      this.next(to);
    }
  },
  clearVideo: function() {
    this.pane.down('div.video_column div.container').update(new Element('div', {id:this.pane.id + "-video"}));
  },
  loadVideo: function() {
    this._disableTransformsToPreventWebKitBug();
    var that = this;
    var videoUrl = this.pane.down("div.video_column").getAttribute("video_url");
    var container = this.pane.down("div.video_column div.container");
    
    var flashvars = {};
    var params = {allowfullscreen: 'true', wmode: 'opaque'};    
    var attributes = {};
    
    swfobject.embedSWF(videoUrl, this.pane.id + "-video", "266", "170", "10.0.0", "/flash/expressinstall.swf", flashvars, params, attributes, function(status) {
      if (status.success) {
        window.setTimeout(function() {
          that._hideLoadingScreen();
        },1500);
      }
      else {
        that._hideLoadingScreen();        
      }
    });
  },
  disableToggleWhileTransitioning: function(length) {
    var that = this;
    this.transitioning = true;
    window.setTimeout(function() {
      that.transitioning = false;
    }, length);
  },
  _setupObservers:function() {
    this.frame = $("guides");
    this.dots = this.frame.down('ul.dots');
    this.bottom = this.frame.down("div.guide-bottom");
    this.pane = this.frame.select("div.content").select(function(p) {
      return p.visible();
    }).first();

    this.pages = [];
    
    var that = this;
    this.frame.select("div.content").each(function(p) { that.pages.push(p); });
    
    // update the current page's next link with the next page's title
    for (var i=0; i < this.pages.size(); i++) {
      try { this.pages[i].down("span.next a").update(this.pages[i+1].down("h5").innerHTML); 
        this.pages[i].down("span.next a").observe('mouseover', this._nextMouseOver.bindAsEventListener(this));
        this.pages[i].down("span.next a").observe('mouseout', this._nextMouseOut.bindAsEventListener(this));
      }
      catch(e) { this.pages[i].down("span.next").hide(); }
    }
    
    if (this.dots) {
      this.dots.observe('mouseover', this._dotsMouseOver.bindAsEventListener(this));
      this.dots.observe('mouseout', this._dotsMouseOut.bindAsEventListener(this));
    }
    this.buttons.close = this.frame.down("a.close");
    this.buttons.next = this.frame.down("a.next"); 
    this.buttons.previous = this.frame.down("a.previous"); 
    
    this._updateNavigation();
  },
  _dotsMouseOver: function(event) {
    var element = event.element();
    if (element.tagName == "A") {
      if (element.up().nextSiblings().invoke("down").pluck("className").include("selected")) {
        this.frame.addClassName('hover-previous');        
      }
      else if (element.up().previousSiblings().invoke("down").pluck("className").include("selected")) {
        this.frame.addClassName('hover-next');
      }
    }
  },
  _dotsMouseOut: function(event) {
    var element = event.element();
    this.frame.removeClassName('hover-next');
    this.frame.removeClassName('hover-previous');    
  },
  _nextMouseOver: function() {
    this.frame.addClassName('hover-next');    
  },
  _nextMouseOut: function() {
    this.frame.removeClassName('hover-next');
  },
  _clearHoverState: function() {
    this.frame.removeClassName('hover-next');    
  },
  _slidePanels: function(incomingPanel, slideDirection) {
    var current = this.pane;
    var width = this.pane.getWidth();
    
    if (slideDirection == "left") {
    }
    else if (slideDirection == "right") {
      width = width * -1;
    }
    
    incomingPanel.setStyle("position:absolute; top:0; left: " + width * 2 + "px;");
    this.pane = incomingPanel;
        
    new Effect.Parallel([
      new Effect.Fade(current, {sync: true}),
      new Effect.Appear(incomingPanel, {sync: true}),
      new Effect.Move(current, {y:0, x: (width * -1), mode:'absolute', sync:true}),
      new Effect.Move(incomingPanel, {y:0, x:0, mode:'absolute', sync: true})
    ], {duration:0.3});
    
    this._showLoadingScreen();
    this._updateNavigation();
    this.loadVideo();
    
    return incomingPanel;
  },
  _updateNavigation: function() {
    this._getPageNumber(this.pane);
    if (this.dots) {      
      this.dots.select('a').invoke('removeClassName', 'selected');
      this.dots.select('a')[this._getPageNumber(this.pane) - 1].addClassName('selected');
    }

    this.buttons.previous[(this.pane.previousSiblings().size() > 0) ? "show" : "hide"]();
    this.buttons.next[(this.pane.nextSiblings().size() > 0) ? "show" : "hide"]();
  },
  _disableTransformsToPreventWebKitBug: function() {
    $(document.body).addClassName("disabletransforms");
  },
  _enableTransformsForWebKit: function() {
     $(document.body).removeClassName('disabletransforms');
  },
  _showLoadingScreen: function() {
    this.pane.addClassName("loading");    
  },
  _hideLoadingScreen: function() {
    this.pane.removeClassName("loading");
  },
  _getPageNumber: function(page) {
    page = $(page);
    return parseInt(page.classNames().toString().match(/page\-([0-9])/)[1], 10);
  }
};


var TipManager = {
  initialize: function() {
    if (typeof(Tips) == "undefined") return;
    
    this.events = {
      addNew: this.addNew.bindAsEventListener(this),
      repositionTips: this.repositionTips.bindAsEventListener(this),
      tipShown: this._tipShown.bindAsEventListener(this),
      tipHidden: this._tipHidden.bindAsEventListener(this),
      tipClosed: this._tipClosed.bindAsEventListener(this),
      ajaxOnComplete: this._ajaxOnComplete.bindAsEventListener(this),
      adjustZIndexes: this._adjustZIndexes.bindAsEventListener(this),
      hideAll: this.hideAll.bindAsEventListener(this)
    };
    
    document.observe('dom:loaded', this.events.addNew);
    document.observe("dom:updated", this.events.repositionTips);
    document.observe('prototip:shown', this.events.tipShown);
    document.observe('prototip:hidden', this.events.tipHidden);
    document.observe('prototip:closed', this.events.tipClosed);
    document.observe('ajax:completed', this.events.ajaxOnComplete);
    document.observe('ajax:completed', this.events.adjustZIndexes);
    SPICEWORKS.observe('pivot:shown', this.events.adjustZIndexes);
    SPICEWORKS.observe('pivot:hidden', this.events.hideAll);
    
    // OPTIMIZE: when we add more prototips throughout the app, this might cause an issue,
    // but for now it's better than the alternative of having a stuck tip.  Finding the tip
    // that is a decendant of this.menu and asking it to hide doesn't seem to work in IE7
    
    Event.observe(window, 'resize', this.repositionTips.bindAsEventListener(this));    
    this.addNew();
  },
  
  unload: function () {
    document.stopObserving('dom:loaded', this.events.addNew);
    document.stopObserving("dom:updated", this.events.repositionTips);
    document.stopObserving('prototip:shown', this.events.tipShown);
    document.stopObserving('prototip:hidden', this.events.tipHidden);
    document.stopObserving('prototip:closed', this.events.tipClosed);
    document.stopObserving('ajax:completed', this.events.ajaxOnComplete);
    document.stopObserving('ajax:completed', this.events.adjustZIndexes);
    SPICEWORKS.stopObserving('pivot:shown', this.events.adjustZIndexes);
    SPICEWORKS.stopObserving('pivot:hidden', this.events.hideAll);    
  },
  
  addNew: function() {
    var that = this;
    $$('a[tip], span[tip]').each(function(element) {
      var options = element.readAttribute('tip').evalJSON();
      that.newTip(element, options);
    });
  },
  newTip: function(selector, options) {
    // Don't change the arguments of this to be (selector, content, options) like Tip() accepts,
    // because in the cases of ajax tips, content is not specified

    var element = $$$(selector);
    if (!element) return; 
    
    if (element.prototip) {
      if (options.forceRedraw) {
        element.prototip.remove();              
      }else {
        if ((options.show) && (!element.prototip.tipObject.wrapper.visible())) {
          // only call the show method if the element isn't already visible
          // in Firefox calling show twice will shift the tip up about 15px
          // TODO: find out why
          element.prototip.show();
        }          
        return; //tip has already been added, don't redraw it
      }
    }
                           
    // if there were duplicates on the page, remove them too
     this.tipsWithSelector(selector).each(function(tip) { 
       Try.these(
         function(tip) { tip.remove(); },
         function(tip) { tip.element.prototip.remove(); }
      );
     });
    
    //if (options.tipID && $(options.tipID)) $(options.tipID).remove();
    
    if (element.up('div.pivotable') ) { // add prototips to pivot menus, add a span around the a element to show the talk bubble icon
      if (!element.up('span.hint')) element.wrap('span', { 'class': 'hint' });
    }
    else {
      options = Object.extend({style:'default'}, options || {});
    }
            
    var tip = this._createTip(element, options);
    
    if (options.show) element.prototip.show();  
    if (options.scrollingParent) { this._setupScrollingHandler(tip); }
    if (options.collapsable) { this._makeCollapsable(tip); }
  
    return tip;
  },
  _makeCollapsable: function(tipObject) {
      // Some of the work to make these happens happens in the initialize method wrap in prototip.js      
      var collapsableOptions = Object.extend({allowOverflow: true, autoExpand: true}, tipObject.options.collapsable);
      if (collapsableOptions.allowOverflow) {
        tipObject.wrapper.addClassName("prototip-collapsable-with-overflow");      
      }
      tipObject.tip.writeAttribute('expandedstyle', Object.toJSON(collapsableOptions.expandedStyle));
      if (collapsableOptions.autoExpand) {
        $(tipObject.borderFrame).observe('mouseover', function() { tipObject.expand(); return false; });
        $(tipObject.borderFrame).observe('mouseout', function() { tipObject.collapse(); return false; });        
      }
      else {
        $(tipObject.borderFrame).observe( 'click', function() { tipObject.toggleExpansion(); return false; });
      }
      
      tipObject.tip.hide();
  },
  _setupScrollingHandler: function(tip) {
    /* hides tip when the target is out of view if scrollingParent has been defined */
    $(tip.options.scrollingParent).observe('scroll', function(tip) { 
      if (tip.options.target.scrolledIntoView(tip.options.scrollingParent)) {
        tip.show();
        tip.reposition();
        if (tip.options.tags) this._fireTagEvents(tip.options.tags, "repositioned", tip.options);
      }
      else {
        tip.hide();
      }
    }.bind(this, tip));
  },
  scrollToTip: function(target) {
    target = $$$(target);
    if (typeof(Tips) != "undefined" && Tips.tips) {
      Tips.tips.each(function(t) {
        t.collapse();
      });
    }
    //, onComplete: $(target).prototip.expand()
    if (target.prototip) Effect.ScrollTo($(target.prototip.tipObject.wrapper), { duration: 0.2 });
  },
  repositionTips: function() {    
    if (typeof(Tips) != "undefined" && Tips.tips) {
      Tips.tips.each(function(t) {
        try {
          t.reposition(); 
          this._tweakBrowserPositioning(t.target);
          this._fireTagEvents(t.options.tags, "repositioned", t.options);          
        }
        catch(e) {  }
      }.bind(this));
    }
  },
  hideAll: function() { 
    if (typeof(Tips) != "undefined" && Tips.visible) {
      Tips.visible.each(function(tip) { 
        if (!tip.hasClassName("sticky")) {
          tip.hide();
        }
      });
    }
  },
  hideChildrenOf: function(selector) {
    if (typeof(Tips) != "undefined" && Tips.tips) {
      Tips.tips.select(function(tip) { return tip.element.up(selector); }).invoke('hide');          
    }
  },
  hideTip: function(id) {
    $(id).prototip.hide();
  },
  hideTipsWithTag: function(tagName) {
    this.tipsWithTag(tagName).each(function(tip) {
      tip.target.prototip.hide();
    });
  },
  removeTipsWithTag: function(tagName) {
    this.tipsWithTag(tagName).each(function(tip) {
      tip.target.prototip.remove();
    });
  },
  showTipsWithTag: function(tagName) {
    this.tipsWithTag(tagName).each(function(tip) {
      tip.target.prototip.show();
    });
  },
  removeTip: function(element) {
    element = $(element);
    if (element.up('div.pivotable') && (element.up('span.hint'))) {
        // remove the a span around the a element to remove the talk bubble icon
        var content = element.innerHTML;
        element.up('li').update(element);
        element.update(content); //ie7 chops the text out, so we have to put it back in
    }
    if (element.prototip) element.prototip.remove();
  },
  tipsWithSelector: function(selector) {
    if (typeof(Tips) == "undefined") return $A();
    var tipsWithSelector = Tips.tips.select(function(tip) {
       if (tip.options.element) {
         return (tip.options.element) == selector; 
       };
    }.bind(this));
    return tipsWithSelector;
  },
  tipsWithTag: function(tagName) {
    if (typeof(Tips) == "undefined") return $A();
    var tipsWithTags = Tips.tips.select(function(tip) {
       if (tip.options.tags) {
         return (tip.options.tags).include(tagName); 
       };
    }.bind(this));
    return tipsWithTags;
  },
  _fireTagEvents: function(tags, action, options) {
    if (tags) {
      $A(tags).each(function(tag) {
        document.fire("prototip:" + tag + ":" + action, options); 
      });
    }    
  },
  removeOrphaned: function() {
    if (typeof(Tips) == "undefined") return;
    Tips.tips.each(function(tip) { 
      if ($(tip.options.target).isOrphaned()) {
        $(tip.options.target).prototip.remove();
      }
    }.bind(this));
  },
  _tipShown: function(event) {
    var element = event.element(), options;
    if (element.prototip)  {
      options = element.prototip.tipObject.options;
      if (options.tags) this._fireTagEvents(options.tags, "shown", options);
      this._fixForIE(element, options);
      this._tweakBrowserPositioning(element);
      this._adjustZIndex(element, options);
    }
  },
  _tipHidden: function(event) {
    var element = event.element(), options;
    if (element)  options = element.prototip.tipObject.options;
    if (options.tags) this._fireTagEvents(options.tags, "hidden", options);
    this._adjustZIndex(element, options);
  },
  _tipClosed: function(event) { 
    var element = event.element();  // the tipClosed event wasn't built into prototip, so I added it and 
    var options = event.memo;       // included a better memo: the options hash used to build the tip
    if (options.tags) this._fireTagEvents(options.tags, "closed", options);
  },
  _createTip: function(element, options) {
    if (typeof(Tip) == "undefined") return;
    
    if (options.ajax) {
      return new Tip($(element), options);
    } 
    else {
      return new Tip($(element), options.content, options);
    } 
  },
  _tweakBrowserPositioning: function(element) {  },
  _adjustZIndexes: function(event) {    
    Tips.tips.pluck('target').each(function(tip) {
      try {
        this._adjustZIndex(tip, tip.prototip.tipObject.options);
      } catch(e) {}
    }.bind(this));
  },
  _adjustZIndex: function(element, options) {
    if (!element.prototip) return;
    var tipObject = element.prototip.tipObject;
    tipObject.wrapper.setStyle("z-index:" + options.zIndex);
  },
  _fixForIE: function(element, options) {
    if (!Browser.ie6) return;
    if (element.prototip) {
      var closeButton = element.prototip.tipObject.wrapper.down('div.close');
      if (closeButton) {
        closeButton.style.filter = closeButton.style.filter.replace(/scale/, 'image'); 
        closeButton.stopObserving('mouseover');  
        closeButton.stopObserving('mouseout');  
      }
    }
  },
  _ajaxOnComplete: function(){
    this.addNew();
    this.removeOrphaned();
  }
};
Event.register(TipManager);


var IntroHelp = {
  helpTips: null, //set on page load
  enabled: true,
  initialize: function() {
    this.events = {
      refreshTips: this.refreshTips.bindAsEventListener(this),
      tipShown: this._tipShown.bindAsEventListener(this),
      tweakBrowserPositioning: this._tweakBrowserPositioning.bindAsEventListener(this),
      tipHidden: this._tipHidden.bindAsEventListener(this),
      tipClosed: this._tipClosed.bindAsEventListener(this),
      disable: this.disable.bindAsEventListener(this),
      enable: this.enable.bindAsEventListener(this)
    };
    
    // Monitor for page changes so we can hide irrelevant tips
    SPICEWORKS.observe("app:helpdesk:ticket:edit:start", this.events.refreshTips);
    SPICEWORKS.observe("app:helpdesk:ticket:edit:finish", this.events.refreshTips);
    SPICEWORKS.observe("app:inventory:group:item:edit:start", this.events.refreshTips);
    SPICEWORKS.observe("app:inventory:group:item:edit:finish", this.events.refreshTips);
    SPICEWORKS.observe("editable-table:start-edit", this.events.refreshTips);
    SPICEWORKS.observe("editable-table:add-new", this.events.refreshTips);
    SPICEWORKS.observe("editable-table:save-edit", this.events.refreshTips);
    SPICEWORKS.observe("editable-table:cancel-edit", this.events.refreshTips);
  
    document.observe('prototip:introhelp:shown', this.events.tipShown);
    document.observe('prototip:introhelp:shown', this.events.tweakBrowserPositioning);
    document.observe('prototip:introhelp:hidden', this.events.tipHidden);  
    document.observe('prototip:introhelp:closed', this.events.tipClosed);    
    document.observe('prototip:introhelp:repositioned', this.events.tweakBrowserPositioning);
    
    document.observe('ajax:completed', this.events.refreshTips);
    
    
    // Disable introhelp when a flyover is shown.  Things go crazy, otherwise
    SPICEWORKS.observe('app:flyover:shown', this.events.disable);
    SPICEWORKS.observe('app:flyover:hidden', this.events.enable);
    SPICEWORKS.observe('app:email_form:hidden', this.events.enable);
    SPICEWORKS.observe('app:email_form:show', this.events.disable);

    if (Prototype.Browser.IE) {
      // HACK. See bug #11186
      // This is to prevent a collision with the initial page load call of loadHelpTips, and the ajax:completed event 
      // calling loadHelpTips and throwing an unspecified error in IE.
      window.setTimeout(this.loadHelpTips, 1000);
    }
    else {
      this.loadHelpTips();
    }
  },
  
  unload: function () {
    // Monitor for page changes so we can hide irrelevant tips
    SPICEWORKS.stopObserving("app:helpdesk:ticket:edit:start", this.events.refreshTips);
    SPICEWORKS.stopObserving("app:helpdesk:ticket:edit:finish", this.events.refreshTips);
    SPICEWORKS.stopObserving("app:inventory:group:item:edit:start", this.events.refreshTips);
    SPICEWORKS.stopObserving("app:inventory:group:item:edit:finish", this.events.refreshTips);
    SPICEWORKS.stopObserving("editable-table:start-edit", this.events.refreshTips);
    SPICEWORKS.stopObserving("editable-table:add-new", this.events.refreshTips);
    SPICEWORKS.stopObserving("editable-table:save-edit", this.events.refreshTips);
    SPICEWORKS.stopObserving("editable-table:cancel-edit", this.events.refreshTips);
    document.stopObserving('prototip:introhelp:shown', this.events.tipShown);
    document.stopObserving('prototip:introhelp:shown', this.events.tweakBrowserPositioning);
    document.stopObserving('prototip:introhelp:hidden', this.events.tipHidden);  
    document.stopObserving('prototip:introhelp:closed', this.events.tipClosed);    
    document.stopObserving('prototip:introhelp:repositioned', this.events.tweakBrowserPositioning);
    document.stopObserving('ajax:completed', this.events.refreshTips);
    SPICEWORKS.stopObserving('app:flyover:shown', this.events.disable);
    SPICEWORKS.stopObserving('app:flyover:hidden', this.events.enable);
    SPICEWORKS.stopObserving('app:email_form:hidden', this.events.enable);
    SPICEWORKS.stopObserving('app:email_form:show', this.events.disable);
  },
  
  disable: function() {
    this.enabled = false;    
    TipManager.removeTipsWithTag('introhelp');
  },
  enable: function() {
    this.enabled = true;
    this.refreshTips();
  },
  loadHelpTips: function() {
    if (!this.enabled) return;
    
    var owner;
    if (this.helpTips) {
       this.helpTips.each(function(instructions) {
         owner = $$$(instructions.owner);
         if (owner && owner.visible()){
            TipManager.newTip(instructions.element, instructions);
         }
       });
    }
    if (Browser.ie6) {
      // Solves problem trying to position on a table.  I hate you, IE6.
      window.setTimeout("TipManager.tipsWithTag('introhelp').invoke('reposition')", 200);
    }
  },
  refreshTips: function() {
    if (!this.enabled) return;
    
    this.removeOrphaned();
    TipManager.removeOrphaned();
    this.loadHelpTips();
    this.hideTipsWithHiddenParents();
    this.hideTipsScrolledOutOfView();    
  },
  removeOrphaned: function() {
    if (typeof(Tips) != "undefined" && Tips.tips) {
      TipManager.tipsWithTag('introhelp').each(function(tip) {
        
        if ($(tip.element).isOrphaned())
          TipManager.removeTip(tip.element);
        else if (!$(tip.element).visible()) 
          TipManager.hideTip(tip.element); //hide tips whose elements are not visible          
        
        if (tip.options.owner) {
          var owner = $$$(tip.options.owner);
          if ((!owner) || (owner.isOrphaned()))
            TipManager.removeTip(tip.element);
          else if (!owner.visible())
            TipManager.hideTip(tip.element);
        }
      }.bind(this));        
    }
  },
  hideTipsScrolledOutOfView: function() {
    if (typeof(Tips) != "undefined" && Tips.tips) {
      TipManager.tipsWithTag('introhelp').each(function(tip) {
        if (tip.options.scrollingParent) {
          if (!tip.options.target.scrolledIntoView(tip.options.scrollingParent)) {
            tip.hide();
          }
        }
      });
    }
  },  
  hideTipsWithHiddenParents: function() {
    if (typeof(Tips) != "undefined" && Tips.tips) {
      TipManager.tipsWithTag('introhelp').each(function(tip) {
        if (!tip.element.visibleOnPage()) {
          TipManager.hideTip(tip.element);
        }
      });
    }
  },
  _tipShown: function(event) {
    var element = $$$(event.memo.element), options = event.memo;
    this._addTitleToCloseButton(element, options);
    this._fixForIE(element, options);
    this._fixWideTipIssue(element, options);
  },
  _tipHidden: function(event) {},
  _tipClosed: function(event) {
    var element = event.memo.element, options = event.memo;
    if (options.traitID) { 
      // remove this tip from the array so it doesn't get added again on the ajax:completed event
      this.helpTips = this.helpTips.reject(function(tip) { return (tip.traitID == options.traitID); }.bind(this));
      new Ajax.Request('/help/hide_tip/', { parameters: { id: options.traitID } } );
    }
  },
  _tweakBrowserPositioning: function(event) {
    var element = $$$(event.memo.target), options = event.memo;   
    if (!element.prototip) return;
    
    var tipObject = element.prototip.tipObject, px=0;
    
    if (Prototype.Browser.Gecko) px = 15;
    else if (Prototype.Browser.WebKit) px = 7;   
    else if (Prototype.Browser.IE) px = 6;   
     
    var top = parseInt(tipObject.wrapper.style.top, 10);
    tipObject.wrapper.setStyle("top: " + (top + px) + "px");
  },
  _fixWideTipIssue: function(element, options) {
    // Sometimes a tip will show up really really wide, and will inexplicably have its 
    // width set to 1000px +.  When this happens we just have to retry.  
    var tipObject = element.prototip.tipObject;
    var target = tipObject.target;
    if (tipObject.tooltip.getWidth() > 500) {
      var rebuildThese = this.helpTips.select(function(tip) { return ($$$(tipObject.element).id == $$$(tipObject.element).id); }.bind(this));
      
      rebuildThese.each(function(instructions) {        
        if (element.prototip) element.prototip.remove();
        var rebuilt = tipObject.options.rebuilt;      
        rebuilt = (!rebuilt ? 1 : rebuilt + 1); 
        if (rebuilt < 5) {
          TipManager.newTip(instructions.element, Object.extend(instructions,{rebuilt: rebuilt}));          
        }
      }.bind(this));      
    }
  },
   _addTitleToCloseButton: function(element, options) {
     var tipObject = element.prototip.tipObject;
     var closeButton = tipObject.wrapper.down('div.close');
     
     closeButton.writeAttribute("title", "Dismiss");
   },
  _fixForIE: function(element, options) {
    if (!Browser.ie6) return;
    
    var tipObject = element.prototip.tipObject;
    var closeButton = tipObject.wrapper.down('div.close');
    closeButton.setStyle('position:absolute;');
    
    // make the title display correctly.  Styles are set inline by prototip, so we have to override them 
    
    // set parents to very small width, forcing title to be as small as it can with the content
    var title = tipObject.title;
    var titleParents = title.ancestors().select(function(el) { return (el.tagName != "BODY" && el.tagName !="HTML"); });
    titleParents.invoke('setStyle', 'width:1%!important;');
  
    // find that smallest size, and set the parents to it so the borders show up correctly
    var titleWidth = title.getWidth();
    titleParents.invoke('setStyle', 'width:' + titleWidth + 'px!important;');
    
    // add 4 pixels to the outer ul.  wow, this is a lot of effort for IE6.  
    title.up('ul').setStyle('width:' + (titleWidth + 4) + 'px');
  
    // find that smallest size, and set the parents to it so the borders show up correctly
    var titleWidth = title.getWidth();
    titleParents.invoke('setStyle', 'width:' + titleWidth + 'px!important;');

    // find that smallest size, and set the parents to it so the borders show up correctly
    var titleWidth = title.getWidth();
    titleParents.invoke('setStyle', 'width:' + titleWidth + 'px!important;');
  }
};
Event.register(IntroHelp);

var Toolbar = {
  options: {},
  initialize: function(options){
    this.options = Object.extend(this._defaultOptions, options || {});

    this._attachObserverCollection(this._inventoryObservers);
    
    if (this.options.inventoryMode == 'browse') this.options.sourcedFrom = 'browse';
    
    SPICEWORKS.observe('app:scan:started', this.scanStarted.bindAsEventListener(this));
    SPICEWORKS.observe('app:scan:stopped', this.scanStopped.bindAsEventListener(this));
    
    this.toggleActions();
  },
  renderComparison: function(left_id, right_id, category, dual_select){
    var postBody;

    LoadingMessage.set('compare_table_wrapper', "Comparing Devices…");

    if (left_id) postBody = 'first_device='+left_id+'&category='+category; // this is always the case if we're in "icon mode"
    else postBody = 'no_devices=true&category='+category; // this can occur if we're in "browse mode" and a device hasn't been selected yet

    if (right_id) postBody += '&second_device='+right_id;
    
    // This function only gets called when the compare panel is already visible
    postBody += '&already_comparing=true';
    if(dual_select) postBody += '&dual_select=true';

    new Ajax.Request('/asset/compare', { parameters:postBody});
    this.mode = 'compare';
  },
  scanStarted: function(){
    var scanLink = $('toggle_scan_link');
    if (scanLink){
      scanLink.removeClassName('not_scanning').addClassName('scanning').update('<span class="inner icon">Stop Scan</span>');
      scanLink.setAttribute("title", "Stop the current network scan");
    }
  },
  scanStopped: function(){
    var scanLink = $('toggle_scan_link');
    if (scanLink)
      scanLink.removeClassName('scanning').addClassName('not_scanning').update('<span class="inner icon">Start Scan</span>');
      scanLink.setAttribute("title", "Start new network scan");
  },
  update: function(options, urlOptions){
    urlOptions = Object.extend({updateUrl: true, merge: true}, urlOptions || {});
    
    var anchorParams;    
    this.options = Object.extend(this.options || {}, options || {});
    
    this.options.activeMode = null;
    this.updateSwitcherLinks();
    this.toggleActions();
    
    if (this.options.inventoryMode  == 'browse') {
      anchorParams = {path: this.options.path};
    }
    else {
      anchorParams = {id: this.options.node };
    }
    
    if (urlOptions.updateUrl) {
      Application.updateUriAnchorParams(anchorParams, {merge: urlOptions.merge});      
    }
  },
  updateSwitcherLinks: function(options) {
    var newUrl = '', nodeID = (this.options.nodeBaseType ? this.options.node : "");
      
    if (!this.options.baseCategory && !this.options.node) {
      newUrl = '/inventory/';          
    }
    else if (this.options.baseCategory == "events") {
      newUrl = "/inventory/groups/devices/" + (nodeID ? nodeID : '');  
    }
    else {          
      newUrl = "/inventory/groups/" + this.options.categoryID + "/" + (nodeID ? nodeID : '');  
    }
        
    var anchorString = Application.buildUriAnchorQueryString($H({id: nodeID}).compact());

    if ($('icon_switcher_view')){
      $('icon_switcher_view').href = newUrl + "?view=icon" + anchorString;
    }    
    
    if ($('icon_switcher_browse')){
      $('icon_switcher_browse').href = newUrl + "?view=browse" + anchorString;
    }
    
    if ($('icon_switcher_list')){
      $('icon_switcher_list').href = newUrl + "?view=list" + anchorString;
    }
    
  },
  copyAction: function(event){
    event.stop();
    this.renderActions('copy', this.options.node);
  },
  reclassifyAction: function(event){
    event.stop();
    this.renderActions('reclassify', this.options.node);
  },
  deleteAction: function(event){
    event.stop();
    this.renderActions('delete', this.options.node);
  },
  renderActions: function(mode, selected_id){
    if (!this.actionEnabled(mode)) { return; }
    
    var postBody;
    if (selected_id) postBody = 'selected_id='+selected_id;
    else postBody = 'no_devices=true';

    postBody += '&mode='+mode+'&category='+encodeURI(this.options.baseCategory)+'&from='+this.options.sourcedFrom;
    new Ajax.Request('/asset/actions', { parameters:postBody });
  },
  generatePopup: function(id, title, content){ new Popup(id, title, '<p>' + content + '</p>', {closeable:true}); },
  
  actionEnabled: function(action) {
    if (!this.options.canPerformActions) { return false; }
    
    return true;
  },      
  toggleActions: function() {
    if ($("bulk_copy_action")) { $("bulk_copy_action")[this.actionEnabled('copy') ? 'removeClassName' : 'addClassName']('disabled'); }
    if ($("bulk_reclassify_action")) { $("bulk_reclassify_action")[this.actionEnabled('reclassify') ? 'removeClassName' : 'addClassName']('disabled'); }
    if ($("bulk_delete_action")) { $("bulk_delete_action")[this.actionEnabled('delete') ? 'removeClassName' : 'addClassName']('disabled'); }
  },
  _attachObserverCollection: function(collection){
    if (!this.elements) this.elements = $H(); // this collection will hold a reference to the dom element, so we don't have to continually look it up
    var that = this;
    $H(collection).each(function(pair){
      // store the element reference, keep in mind that the element could be nil
      that.elements.set(pair.key, $(pair.key));
      // if we have the element, then we need to attach the observer
      if (that.elements.get(pair.key)) that.elements.get(pair.key).observe('click', that[pair.value].bindAsEventListener(that));
    });
  },
  
  _defaultOptions:{
    node:null, // the id of an item
    nodeCategory:null, // the category of a device
    baseCategory: null, // the currently selected group.  Node category is usually the same, except in the 
                        // case of installables, where node category = software, hotfix, applications
    nodeType:null,      
    nodeBaseType:null, // the model of a device
    canAddNewAsset:false,
    sourcedFrom:'unknown',
    canPerformActions:false,
    deviceCount: null,
    userTaggable: null,
    groupCount: null,
    activeGroupType: null,
    activeMode:null, // null is steady-state, can also be reclassify, compare, etc. which are transient states
    inventoryMode:false, // can also be 'icon', 'browse'
    helpDeskMode:false,
    ticketID:null
  },
   // these observers use the element ID as the key and the name of the method to invoke as the value
  _inventoryObservers:{
    'bulk_reclassify_action' : 'reclassifyAction',
    'bulk_delete_action' : 'deleteAction',
    'bulk_copy_action' : 'copyAction'
  }
};

var CheckboxToggler = {
  toggle: function(selector, makeChecked){
    // sets the state of all checkboxes that match "selector" to the boolean value of "makeChecked"
    var checkboxes = $$(selector);
    if (checkboxes){
      checkboxes.each(function(checkbox){
        checkbox.checked = makeChecked;
      });
    }
  }
};
(function(context){
  function toggle(control, collection){
    if (!control.hasClassName('check-toggler')) control = control.up('.check-toggler');
    var turnOn = true;
    if (control.getAttribute('data-checkstate') == 'on') turnOn = false;
    collection.each(function(checkbox){
      if (checkbox.getAttribute('type') != 'checkbox') return;
      checkbox.checked = turnOn;
    });
    control.setAttribute('data-checkstate', turnOn ? 'on' : 'off');
  };
  context.check = {
    toggle:toggle
  };
})(window);

var ReclassifyIndividual = {
  toggle: function(){
    var answer = $('reclassify_answer');
    var answer_custom = $('reclassify_answer_custom');
    if (answer.visible()){
      answer.hide();
      answer_custom.hide();
      $$('#reclassify_device input.removers').invoke('show');
    } else {
      answer.show();
      answer_custom.hide();
      $$('#reclassify_device input.removers').invoke('hide');
    }
  },
  addCustom: function(device_id){
    // get the value from 'custom_reclassify'
    // send an ajax call to 'reclassify'
    Form.Element.disable('save_custom_type');
    $('reclass_message').update('Reclassifying this device...');
    var postBody = 'id=' + device_id + '&type=' + escape($('custom_type').value);
    postBody += '&original_category=' + encodeURI(Toolbar.options.baseCategory) + '&from=' + Toolbar.options.sourcedFrom;
    new Ajax.Request('/asset/reclassify', {parameters:postBody});
  },
  deleteDevice: function(base_url){
      var postBody = 'original_category='+encodeURI(Toolbar.options.baseCategory)+'&from='+Toolbar.options.sourcedFrom;
      new Ajax.Request(base_url, {parameters:postBody });
  }
  
  
};

var AjaxSlideShow = Class.create();
AjaxSlideShow.prototype = {
  activeSlide:null,
  activeChart:null,
  initialize: function( slides, showID, options ){
    this.options = Object.extend({
      slideDuration:10,
      ajaxParameters: {
        slideshow:true
      }
    }, options || {});

    this.slides = $A(slides);

    // decode double-escaped URLs that can be passed in because Rails 2.0 likes to do that
    var that = this;
    this.slides.each(function(slide, index){
      if (slide.include('&amp;')) that.slides[index] = slide.unescapeHTML();
    });

    this.showID = showID;
    this.activeSlide = 0;
  },
  refresh: function(){
    this.playbackControl = $( this.showID + '_playback' );
  },
  destroy: function(){
  },
  next: function(){
    this._loadSlide( ++this.activeSlide );
  },
  previous: function(){
    this._loadSlide( --this.activeSlide );
  },
  _loadSlide: function( slide ){
    if ( slide >= this.slides.size() ){
      // the slideshow has completed one full loop, let's pause and put it back at the first slide
      slide = 0;
    } else if ( slide < 0 ) slide = this.slides.size() - 1;
    
    this.activeSlide = slide;
    
    this.activeRequest = new Ajax.Request( this._sanitizeSlideUrl( this.slides[ this.activeSlide] ), { parameters: this.options.ajaxParameters } );
  },
  _sanitizeSlideUrl: function( url ){
    return url.replace( /&amp;/, '&' );
  }
};

var AjaxSlideShowManager = {
  shows:$H(),
  initialize: function(){
  },
  create: function( showID, slides, options ){
    if ( ! this.shows.get(showID) ) this.shows.set(showID, new AjaxSlideShow( slides, showID, options ) );
  },
  destroyChartIfPresent: function( showID ){
    if ( this.shows.get(showID) && this.shows.get(showID).activeChart && this.shows.get(showID).activeChart.destroy ) this.shows.get(showID).activeChart.destroy();
  },
  renew: function( showID, slides, options ){    if ( this.shows.get(showID) ) this.shows.get(showID).destroy();
    this.shows.set(showID, new AjaxSlideShow( slides, options ));
  },
  next: function( showID ){ this._callShow( 'next', showID, true ); },
  previous: function( showID ){ this._callShow( 'previous', showID, true ); },

  _callShow: function( method, showID, arg ){
    if ( this.shows.get(showID) ) this.shows.get(showID)[method]( arg );
  }
};

var PrimaryBreadCrumbs = {
  incrementLastCrumbCount: function(step) {
    var label = $$$("div.sui-header > h1 span.crumb.last > a");
    
    if (label) {
      var match = label.innerHTML.match(/\(([0-9]+)\)/);
      if (match) {
        var count = parseInt(match[1], 10);
        count = count + step;
        label.update(label.innerHTML.replace(/\([0-9]+\)/, "(" + count + ")"));
      }
    }
  }
};

var IconView = {
  initialize:function(categoryName, options){
    this.categoryName = categoryName;
    this.options = Object.extend({
      mode:'hardware',
      automatic:true
    }, options || {});
    this.mode = this.options.mode;

    // the SoftwareList object handles the rest of this for software categories
    if (this.mode == 'software') return;
  
    this.listeners = {
      iconClick:this.iconClicked.bindAsEventListener(this),
      iconMouseover:this.iconMouseover.bindAsEventListener(this),
      iconMouseout: this.iconMouseout.bindAsEventListener(this),
      applicationSearch: this.applicationSearch.bindAsEventListener(this)
    };
    
    SPICEWORKS.observe('app:search:result:selected', this.listeners.applicationSearch);
    SPICEWORKS.observe('app:helpdesk:ticket:opened', this.incrementIndicatorCount.bindAsEventListener(this, 'ticket', 1));
    SPICEWORKS.observe('app:helpdesk:ticket:closed', this.incrementIndicatorCount.bindAsEventListener(this, 'ticket', -1));
    SPICEWORKS.observe('app:alert:created', this.incrementIndicatorCount.bindAsEventListener(this, 'alert', 1));
    SPICEWORKS.observe('app:alert:cleared', this.incrementIndicatorCount.bindAsEventListener(this, 'alert', -1));
    SPICEWORKS.observe("app:scan:error:cleared", this.incrementIndicatorCount.bindAsEventListener(this, "error", -1));
    SPICEWORKS.observe('app:inventory:group:item:removed', this.itemRemoved.bindAsEventListener(this));
    SPICEWORKS.observe('app:inventory:group:item:loading', this.itemLoading.bindAsEventListener(this));
    
    SPICEWORKS.observe("app:inventory:group:item:ready", this.itemLoaded.bindAsEventListener(this));
    Dispatcher.register("click", this.dispatch.bind(this));
  },
  incrementIndicatorCount: function(event, type, increment) {
    var memo = event.memo;
    if (!memo) { return; }    
    if ((memo.assetType == "Device") && (memo.assetId)) {
      var li = $("device_" + memo.assetId);
      if (!li) { return; }
      var selector = "a span.indicators span." + type;
      indicator = li.down("a span.indicators span." + type);
      if (!indicator) { return; }
      
      newCount = (parseInt(indicator.innerHTML, 10) || 0) + increment;
      if (newCount < 0) { newCount = 0; }
      indicator.update(newCount);
      if (newCount == 0) {
        indicator.hide();
        li.removeClassName("filter-" + type + "s");
      }
      else {
        indicator.show();
        li.addClassName("filterable");
        li.addClassName("filter-" + type  + "s");
      }
      SUI.refreshAllFilterbars();
    }
  },
  itemLoading: function(event) {
    var memo = event.memo;
    if (!memo) { return; }
    this.selectOne("device_" + memo['assetId']);
  },
  itemLoaded: function(event) {
    
  },
  selectOne: function(item) {
    this.selectNone();
    item = $(item);
    item.addClassName("selected");
  },
  itemRemoved: function(event) {
    var memo = event.memo;
    if (!memo) { return; }
    if ((memo['assetType'] == "Device") && (memo['assetId'])) {
      $("device_" + memo['assetId']).fade({duration:1});
      PrimaryBreadCrumbs.incrementLastCrumbCount(-1);
    }
  },
  registerAssets: function(container, showInfoBoxes) {
    container = $(container);
    container.observe('click', this.listeners.iconClick);
    if (showInfoBoxes) {
      container.observe('mouseover', this.listeners.iconMouseover);      
      container.observe('mouseout', this.listeners.iconMouseout);
    }
  },
  detachIcon:function(link){
    link = $(link);
    if (this.listeners) {
      link.stopObserving('click', this.listeners.iconClick);
      link.stopObserving('mouseover', this.listeners.iconMouseover);
    }
    AssetPopupManager.release(link.up('li').getAttribute('item_id'));
  },
  applicationSearch: function(event){
    var item = event.memo;
    if (this.selectItem(item)) { event.stop(); }
  },
  dispatch: function(event) {
    var element = event.element();
    if (element.tagName == "A" && element.hasAttribute('dispatch_info')) {
      this.selectItem(element.getAttribute('dispatch_info').parseQuery(), event);
    }
  },
  selectItem:function(item, event) {
    var primary = $('primary');
    var device = primary.down('li#device_' + item.id);
    if (item.model != "Device") { return; }
    if (!device) { return false; }
    
    if (device) {
      primary.select('li.selected').invoke('removeClassName', 'selected');
      device.addClassName('selected');
      Asset.loadSummary(device.getAttribute('click_url'), {id: item.id, type: item.model, tab: item.tab, category: Toolbar.options.nodeCategory});
      if (event) { event.stop(); }
    }
  },
  _getDispatchInfo: function(event) {
    var info, element = event.element();
    if (element.hasAttribute('dispatch_info')) { 
      return element.getAttribute('dispatch_info').parseQuery(); 
    }
    else { 
      return {}; 
    }
  },
  iconMouseover: function(event){
    var element = event.findElement('li');
    if (element) {
      this.loadPopup = window.setTimeout(this.loadAssetPopup.bind(this, element), 200);    
    }
  },
  loadAssetPopup: function(element) {
    AssetPopupManager.loadForAsset(element.getAttribute('item_id'));
  },
  iconMouseout: function(event) {
    var element = event.findElement('li');
    if (element && this.loadPopup) {
      window.clearTimeout(this.loadPopup);
    }
  },
  iconClicked:function(e){
    var clickedLink = e.findElement('a');
    if (clickedLink) {     
      var clickedItem = clickedLink.up('li.icon');
      e.stop();
      if (!clickedItem.hasClassName('unclickable')) {
        this.selectOne(clickedItem);
        Asset.loadSummary(clickedItem.getAttribute('click_url'), {id: clickedItem.getAttribute("item_id"), type: clickedItem.getAttribute("item_model"), name: clickedItem.getAttribute("item_name")});
      }
    }
  },
  showDeviceSummary:function(){
    var deviceSummary = $('item_summary');
    if (deviceSummary && !deviceSummary.visible()) new Effect.BlindDown('item_summary', {duration:0.5});
  },
  myIndex:function(li){
    // returns the value from the 'my_index' attribute of a list item, parsed as an integer so we can do conditional stuff
    var liIndex = 0;
    if (li && li.getAttribute('my_index')) liIndex = parseInt(li.getAttribute('my_index'), 10);
    return liIndex;
  },
  scrollSelectedIntoView:function(selected) {
    if (!selected) {
      selected = $$$('#primary ul li.selected a');
    }
    selected.focus();
    selected.blur();
  },
  selectNone: function() {
    if (Toolbar.options.inventoryMode != "browse") {
      // see bug #13012
     $$('#primary ul li.selected').invoke("removeClassName", "selected");
     Toolbar.options.node = null;      
    }
    Application.updateUriAnchorParams({id: null, tab:null});  
  },
  checkForLinking:function(baseUri){
    baseUri = baseUri.replace("&amp;", "&");
    var matches = null, anchorParams = $H(Application.getUriAnchorParams()), fetchItem = false;
        
    // if no device is pre-selected, then render the first device in the list
    if (!anchorParams.get('id')) {
      if ($('group_summary')) return;
      // show the first icon summary
      var viewer = $$('#primary ul li.icon');
      if (viewer && viewer.length > 0){
        var firstItem = viewer.detect(function(item){
          if (item.visible()) return true;
        });
        if (firstItem){
          fetchItem = true;
          anchorParams.set('id', firstItem.getAttribute('item_id'));
        }
      }
    } else {
      fetchItem = true;
    }
    
    if (!fetchItem) return;
    // If the device is already loaded, don't reload it. (happens when you've got /inventory/groups/servers/5#id-5)
    if ((Toolbar.options) && (anchorParams.get('id') == Toolbar.options.node)) return;
    
    baseUri = baseUri.replace('-model-', anchorParams.get('model'));
    baseUri = baseUri.replace('-id-', anchorParams.get('id'));

    if (anchorParams.get('tab') == "events" && (matches = location.href.match(/pivot_value=(\d{4}-\d{1,2}-\d{1,2})/))) {
      anchorParams = anchorParams.merge({date: matches[1]});      
    }

    anchorParams = anchorParams.merge({initial_load: true});
    baseUri = baseUri + (baseUri.match(/\?/)==null ? "?" : "&") + anchorParams.toQueryString();

    // This function knows these values before the toolbar_update method in toolbar_helper does. If we don't
    // update the toolbar information now, summaryLoaded() will update the URL with incorrect information, ultimately leading to doom    
    Toolbar.update({node: anchorParams.get('id'), nodeType: anchorParams.get('model') });
    
    var selected = $('device_' + anchorParams.get('id')).down('a');
    Asset.loadSummary(baseUri, {id: anchorParams.get('id'), type: anchorParams.get('model'), category: Toolbar.options.nodeCategory});
    this.scrollSelectedIntoView(selected);
  },
  getActiveTab:function() {
    var summary_tabs = $('summary_tabs');
    var active_tab;
    if (summary_tabs) active_tab = summary_tabs.down('.active').id; 
    return active_tab;
  },
  toggleCompareSections:function(cell){
    var row = $(cell.parentNode);
    var section = $('comparison_' + row.getAttribute('for_section'));

    if (row.hasClassName('shown')){
      // hide the section
      row.removeClassName('shown');
      row.addClassName('hidden');
      section.hide();
    } else {
      // show the section
      row.removeClassName('hidden');
      row.addClassName('shown');
      section.show();
    }
  },
  flipDeviceSelect:function(select_id, other_id) {
    var select = SpiceSelectManager.get(select_id); 
    var other = $(other_id);
    if (other.visible()) {
     other.hide();
     other.down('input').clear();
     select.activator.show();
    }
    else {
     select.menu.hide(); 
     select.activator.hide(); 
     other.show();
     other.down('input').clear().focus();
     other.down('input').highlight();
    }
  }
};

var ImageButton = Class.create({
  initialize: function( button, buttonKey ){
    this.button = button;
    this.buttonKey = buttonKey;
    this.button.setAttribute( 'key', this.buttonKey );
    
    this.activeState = 'normal';
    if ( this.button.disabled ) this.activeState = 'disabled';
    
    this.buttonStates = {
      normal: new Image,
      hover: new Image,
      disabled: new Image
    };
    
    this.buttonStates.normal.src = this.button.src.replace('_hover', '').replace('_disabled', '');
    this.buttonStates.hover.src = this.buttonStates.normal.src.replace('.gif', '_hover.gif');
    this.buttonStates.disabled.src = this.buttonStates.normal.src.replace('.gif', '_disabled.gif');
    
    this.events = {
      mouseOver: this.mouseOver.bindAsEventListener( this ),
      mouseOut: this.mouseOut.bindAsEventListener( this )
    };
    this._addObservers();
  },
  setActiveState: function( state ){
    // set to default if an invalid state is passed in...
    if ( ![ 'normal', 'hover', 'disabled' ].include( state ) ) state = 'normal';

    this.activeState = state;
    this.button.disabled = this.disabled();
    this.button.src = this.buttonStates[ this.activeState ].src;
  },

  mouseOver: function(){ this.setActiveState( 'hover' ); },
  mouseOut: function(){ this.setActiveState( 'normal' ); },
  disable: function(){ this.setActiveState( 'disabled' ); },

  normal: function(){ return this.activeState == 'normal'; },
  hover: function(){ return this.activeState == 'hover'; },
  disabled: function(){ return this.activeState == 'disabled'; },
  
  isOrphaned: function(){ return this.button.isOrphaned(); },
  
  destroy: function(){
    this._removeObservers();
    this.button = null;
    this.buttonKey = null;
    this.activeState = null;
    this.buttonStates.normal = null;
    this.buttonStates.hover = null;
    this.buttonStates.disabled = null;
    this.events.mouseOver = null;
    this.events.mouseOut = null;
  },
  _addObservers: function(){
    this.button.observe( 'mouseover', this.events.mouseOver );
    this.button.observe( 'mouseout', this.events.mouseOut );
  },
  _removeObservers: function(){
    this.button.stopObserving( 'mouseover', this.events.mouseOver );
    this.button.stopObserving( 'mouseout', this.events.mouseOut );
  }
});

var ButtonManager = {
  buttons:$H(),
  initialize: function(){
    var that = this;
    $$('input[type=image]').each( function( button ){
      that._attachButton( button );
    });
    
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
  },

  ajaxOnComplete: function(){
    this._removeOrphaned();
    this._attachFreshButtons();
  },
  
  alterStateOfButton:  function( buttonKey, state ){
    var button = this.buttons.get(buttonKey);
    if (button) button.setActiveState(state);
  },
  
  _removeOrphaned: function(){
    var that = this;
    this.buttons.each( function( pair ){
      if (pair.value.isOrphaned()){
        pair.value.destroy();
        that.buttons.unset(pair.key);
      }
    });
  },
  _attachFreshButtons: function(){
    var that = this;
    $$('input[type=image][!key]').each( function( button ){
      if (!button.getAttribute('key')) that._attachButton(button);
    });
  },

  _attachButton: function(button){
    if (button.src.indexOf('_active.gif') > -1) return;
    // attach a button to the collection, keyed by either the button ID or a random number
    var buttonKey = button.id ? button.id : ( Math.random() * 100 ).toString();
    this.buttons.set(buttonKey, new ImageButton( button, buttonKey ));
  }
};

Event.register(ButtonManager);

var LoadingMessage = {
  set: function(el, message, options) {  
    options = Object.extend({}, options || {});       
    options['class'] = (options['class'] || "") + ' loading';
    if (!message) { message = "Loading&hellip;"; }
    StatusMessage.set(el, message, options);
  }
};

var StatusMessage = {
  set: function(el, message, options) {
    options = Object.extend({}, options || {});    
     
    el = $$$(el);
    
    if (!el) { return; };    
    var loadingMessage = new Element('div', {'class': "sui-status-message" + " " + options['class'] }).update(new Element('h3').update(new Element("span").update(message)));
    
    if (options.matchHeight) {
      el.getHeight();
      loadingMessage.setStyle("height: " + el.getHeight() + "px;");
    }
    
    el.update(loadingMessage);
  }
};

var UserForm = {
  selectAccountForm:null,
  manageAccountForm:null,
  accountSelector:null,
  saveButton:null,
  testButton:null,
  newAccountLink:null,
  initialize: function(){

    this.selectAccountForm = $('fix_login_form_select');
    this.newAccountLink = $('create_account_button_helper');
    this.accountSelector = $('selected_account');
    this.saveButton = $('btn_save');
    this.testButton = $('btn_test');

    this.saveButton.observe( 'click', this.saveSettings.bindAsEventListener(this) );
    this.testButton.observe( 'click', this.testSettings.bindAsEventListener(this) );

    this.accountSelector.observe( 'change', this.accountSelected.bindAsEventListener(this) );
    this.newAccountLink.observe( 'click', this.createAccountClicked.bindAsEventListener(this) );
  },
  testSettings: function(event){
    Element.update('test_results', '');

    $('test_results').update ('Testing, please wait...');
    $('user_test').value = 'test';
    
    this.manageAccountForm.onsubmit();
  },
  saveSettings: function(event){
    $('test_results').update( 'Saving account' );
    $('user_test').value = '';
    this.manageAccountForm.onsubmit();
  },
  createAccountClicked: function(event){
    event.stop();
    this.accountSelector.selectedIndex = this.accountSelector.options.length - 1;
    this.accountSelected();
  },
  accountSelected: function(event){
    this.selectAccountForm.onsubmit();
  },
  accountSettingsChanged: function(event){
    Form.Element.disable( this.saveButton );
    Form.Element.enable( this.testButton );
  },
  attachUserForm: function(){
    this.manageAccountForm = $('fix_login_form');
  },
  callbackToRunScan: function( device, account, userType ){
    new Ajax.Request( '/settings/network/rescan_similar_devices/?device_id=' + device + '&account=' + account + '&user_type=' + userType);
  }
};

// used to "search as you type" in a table, such as the applications tab for a group summary or device or the storage tab for a group summary
// this object relies on 
var LiveSearchTable = {
  search:function(table, query){
    if (query == ''){
      this.clear(table);
    } else {
      // this bit of code has been optimized to NOT use anything from prototype (well almost anything) as to speed it up as much as possible
      var searchableCollection = document.getElementById(table).getElementsByTagName('span');
      var search, myRow, selectedNode;
      var reg = new RegExp(query, "i");
      for (var i=0;i<searchableCollection.length;i++){
        if (searchableCollection[i].className.indexOf('searchable') > -1){
          myRow = searchableCollection[i].parentNode.parentNode;
          if (!selectedNode && myRow.className.indexOf('clicked') > -1) selectedNode = myRow;
          search = searchableCollection[i].innerHTML;
          reg.test(search) ? myRow.style.display = '' : myRow.style.display = 'none';
        }
      }
      if (selectedNode && selectedNode.style.display == 'none') $(selectedNode).removeClassName('clicked');
    }
    this.restripe(table);
  },
  clear:function(collection){
    var rows = $(collection).getElementsByTagName('tr');
    for(var i=0;i<rows.length;i++){
      $(rows[i]).show();
    }
  },
  filter:function(toFilter, input, query){
    if ($(toFilter)){
      $(input).value = query;
      if (query == '')
        this.clear();
      else
        this.search(toFilter, query);
    }
  },
  restripe:function(table){
    var counter = 0;
    $(table).select('tbody tr').each(function(row){
      if (row.visible()) row.removeClassName('stripe0').removeClassName('stripe1').addClassName('stripe' + ( counter++ % 2 ? '1' : '0' ));
    });
  },
  keyDown: function(){
    if (window.event && window.event.keyCode == Event.KEY_RETURN) return false;
  }
};

var ApplicationSearch = {
  initialize: function(){
    this.form = $('search_box');
    this.input = $('nav_search');
    this.results = this._buildResultBox();
    this.indicator = this._buildLoadingMessage();
    this.autocompleter = new Ajax.Autocompleter(this.input, this.results, '/search', {
      requestClass:'searchAutocompleter', 
      select:'autocompleter-value', 
      onShow: this.onShow.bind(this),
      indicator: 'search-indicator',
      frequency: 0.01,
      parameters:this._buildSearchContext(), 
      afterUpdateElement:this.resultSelectedAutocompleterCallback.bind(this)});
      
    SPICEWORKS.observe.delay(0.5, 'app:search:result:selected', ApplicationSearch.resultSelected.bindAsEventListener(ApplicationSearch));
  },
  positionResults: function(searchBox, searchResults) {
    if(!searchResults.style.position || searchResults.style.position=='absolute') {
      searchResults.style.position = 'absolute';
      searchResults.style.width = '293px';
      Position.clone(searchBox, searchResults, {
        setHeight: false,
        setWidth:false,
        offsetTop: searchBox.offsetHeight + 1,
        offsetLeft: -80
      });
    }
  },
  onShow: function(element, update) {
    this.positionResults(element, update);
    update.show();
  },
  resultSelectedAutocompleterCallback: function(searchBox, resultItem){
    var payload = {};

    if (resultItem.hasClassName('show-all')) { document.location = resultItem.down('a.meta-url').href; }
    if (resultItem.hasClassName('result-heading')) payload = {item:false, model:resultItem.down('span.meta-search-model').innerHTML, section:resultItem.down('span.meta-search-section').innerHTML};
    else payload = {item:true, model:resultItem.down('span.meta-model').innerHTML, id:resultItem.down('span.meta-id').innerHTML, name:resultItem.down('span.meta-name').innerHTML, fullURL:resultItem.down('a.meta-url').href};
    SPICEWORKS.fire('app:search:result:selected', payload);
  },
  resultSelected: function(event){
    if (event.stopped) return;
    var selected = event.memo;
    if (selected && selected.item && selected.fullURL) document.location.href = selected.fullURL;
    else if (selected.section && selected.model){
      Form.Element.setValue('nav_search_section', selected.section);
      Form.Element.setValue('nav_search_model', selected.model);
      $('search_box').submit();
    }
  },
  _buildLoadingMessage: function() {
    var loading = new Element('div', {'id':'search-indicator', 'class':'auto_complete', 'style':'display:none'});
    var ul = new Element('ul', {'id': 'search-loading-message'});
    ul.insert(new Element('li', {'class': 'show-all selected'}).update("Show All"));
    ul.insert(new Element('li', {'class': 'loading'}).update("Loading…"));
    loading.insert(ul);
    document.body.insert(loading);
    this.positionResults(this.input, loading);
  },
  _buildSearchContext: function(){
    var params = 'search_from=' + escape(document.location.href);
    if (Toolbar.options.categoryID) params += '&inventory_category=' + Toolbar.options.categoryID;
    return params;
  },
  _buildResultBox: function(){    
    document.body.insert('<div id="autocompleter_search_results" class="auto_complete"></div>');   
    // append to the body instead of the form element so it will sit on top of all other content
    return $('autocompleter_search_results');
  }
};

var Notes = {
  stamped:false,
  editing:false,
  stampNote: function(){
    var note = $('note_body');
    if (note.value == ''){
      note.value = Helpers.today() + " - ";
    } else {
      var today = Helpers.today();
      if (note.value.indexOf(today) < 0) note.value += "\n\n" + Helpers.today() + " - ";
    }

    var node_notes = $('node_notes');
    node_notes.removeClassName('editing');

    this.editing = false;
    this.stamped = true;
  },
  edit: function(){
    if (!this.editing){
      var node_notes = $('node_notes');
      node_notes.addClassName('editing');

      this.editing = true;
      var note = $('note_body');
      $('my_notes').hide();
      $('node_notes_form').show();
    
      // scroll down to the bottom of the textarea
      note.scrollTop = note.scrollHeight;
      // give it focus
      note.focus();
    
      // place cursor at end of textbox (adapted from http://www.codecomments.com/archive298-2006-2-820220.html)
      if (note.setSelectionRange) {
        note.setSelectionRange(note.value.length, note.value.length);
      }
      else if (note.createTextRange) {
        var range = note.createTextRange();
        range.collapse(true);
        range.moveEnd('character', note.value.length);
        range.moveStart('character', note.value.length);
        range.select();
      }
      SPICEWORKS.fire("app:notes:edit:start");
    }
  },
  cancel: function(){ this.done(); },
  save: function(url){
    var note_body = $('note_body');
    note_body.disabled = true;
    note_body.addClassName('saving');

    new Ajax.Request(url, { parameters: { 'note[body]': note_body.value } });

    SPICEWORKS.fire("app:notes:edit:start");
  },
  done: function(){
    $('my_notes').show();
    $('node_notes_form').hide();

    var node_notes = $('node_notes');
    node_notes.removeClassName('editing');
    Notes.editing = false;

    var note_body = $('note_body');
    note_body.disabled = false;
    note_body.removeClassName('saving');
    
    SPICEWORKS.fire("app:notes:edit:start");
  }
};

var Panels = {
  flip:function(fadeIn, fadeOut) {
    fadeIn = $(fadeIn);
    fadeOut = $(fadeOut);
    
    new Effect.Fade(fadeOut, {duration:0.2, afterFinishInternal:function(){
      fadeOut.hide();
      fadeOut.setOpacity(1);
      $(fadeIn).appear({duration: 0.2, afterFinishInternal:function(){ 
        fadeIn.show();
      }}); 
    }});
  },
  hide:function() {
    $$("#secondary > div").invoke('hide');
  },
  show: function(el) {
    this.hide();
    $(el).show();
  },
  transition: function(el) {
    el = $(el);
    var visPanels = this.visiblePanels();
    
    if (el.visible()) {
      visPanels.without(el).invoke("fade", {duration:0.2, afterFinishInternal:function(){
        visPanels.invoke('setOpacity', 1);
        visPanels.without(el).invoke('hide');
      }});
    }
    else {
      visPanels.invoke("fade", {duration:0.2, afterFinishInternal:function(){
        visPanels.invoke('hide');
        visPanels.invoke('setOpacity', 1);
        el.appear({duration: 0.2, afterFinishInternal:function(){ 
          el.setOpacity(1);
          el.show();
        }});
      }});
    }
  },
  visiblePanels: function() {
    return ($$("#secondary > div").select(function(a) { return a.visible(); }));
  },
  activePanel: function() {
    return ($("secondary").childElements("div").detect(function(a) { return a.visible(); }));
  },
  activeContent: function() {
    return (this.activePanel().childElements('div').detect(function(a) { return a.visible() && !a.hasClassName("sui-header") && !a.hasClassName("sui-toolbar") && !a.hasClassName("sui-filterbar");}));
  }
};

/* helper methods go here */
var Helpers = {
  innerText:function(element){
    /* 
    This function is a duplicate of a method added to the Element object in prototype.
    */
    element = $(element);
    return element ? (element.innerText ? element.innerText : element.textContent) : '';
  },
  today:function(){
    var d = new Date();
    return d.print(Application.vDateFormat); //(d.getMonth()+1) + "/" + d.getDate() + "/" + d.getFullYear();
  }
};

var TicketManager = {
  checkForNeedToRedraw: function( from ){
    if ( from == 'browse' ){
      // called from the browse page, let's refresh that panel
      this._tryToRedrawInBrowseView();
    } else if ( from == 'fetch_summary_for_item' ) {
      // icon view of devices
      this._tryToRedrawInIconView();
    } else if ( from == 'fetch_installable_summary_for_item' ){
      this._tryToRedrawInListView();
    }
  },
  _tryToRedrawInBrowseView: function(){
    // get any selected item in the browse view
    var selected = $$( '.threecolumnrowhighlight' );
    if ( selected && selected.length > 0 ){
      // get the last selected item
      selected = selected.last();
      Browse.showNextColumn( selected );
    }
  },
  _tryToRedrawInIconView: function(){
    var clicked_item = $$( '#primary li.selected' );
    if ( clicked_item && clicked_item.length > 0 && ( clicked_item = clicked_item[0] ) ){
      IconView.drawSummary( clicked_item.getAttribute('click_url'), clicked_item );
    }
  },
  _tryToRedrawInListView: function(){
    new Ajax.Request( '/software/show', { parameters: { id: Toolbar.options.node, category: Toolbar.options.nodeCategory } } );
  }
};

var AlertManager = {
  dismissAlert: function(alertID){    
    alertID = "alert_" + alertID;  
    var items, hidden, alert = $(alertID);
    if (alert) {
      alert.remove();
    }  
    
    var container = $('item_alerts');
    if (container) {
      items = container.select("ul.alert_list > li");
      if (items.size() == 0) {
        container.fade();
      }
      else {
        hidden = items.select(function(a) { return !a.visible(); });
        if (hidden.size() > 0) {
          hidden[0].show();
        }
      }
    }
  },
  showRecent: function() {
    var alerts =  $("item_alerts").select("ul.alert_list > li");
    alerts.invoke('hide');
    alerts.slice(0,2).invoke('show');
  },
  showAll: function() {
    var alerts =  $("item_alerts").select("ul.alert_list > li");
    alerts.invoke('show');
  }
};

var SimpleProgress = Class.create({
  /* options 
  hidePercent: doesn't show percent at all
  ignoreNegatives: prevents progress bar from shrinking
  showDecimals: shows decimal percent readings
  */
  initialize: function(id, options) {
    this.container = $(id);
    this.progressBar = this.container.down("div.bar");
    this.percentage = this.container.down("span.percentage");
    this.percent=this.percentage.down('span.value');   
    this.lastPercent = 0;
    this.options=options || {};
    this.percentage.setStyle("text-align:right; position:absolute; right:0; display:block;");
    this.percentage.hide();
  },
  update: function(percentComplete) {
    /* don't show the progress bar decreasing */
    if ((percentComplete < this.lastPercent) && (this.options.ignoreNegatives)) return;
    else if (percentComplete < 0) percentComplete = 0;
    
    this.percent.update(percentComplete + " %");
    /*only show it after it has been initiallly positioned */
    if (!this.options.hidePercent) this.percentage.show();   

    var that = this;
    new Effect.Morph(this.progressBar, {style:{width: percentComplete + '%'}, 
      afterFinishInternal:function(){
        if (percentComplete < 100) {
          if ((that.progressBar.hasClassName('still')) || (!that.progressBar.hasClassName('moving'))) { 
            that.progressBar.removeClassName('still');
            that.progressBar.addClassName('moving');
          }
        }
        if (percentComplete >= 100) that._onFinish();
      }
    });
  },

  _onFinish: function() {
    this.progressBar.removeClassName('moving');
    this.progressBar.addClassName('still');
  }
});

var SimpleProgressManager = {
  update: function (key, progress) {
    if ((!this.bars) || (!this.bars.get(key))) return;
    var bar = this.bars.get(key);
    bar.update(progress);
    return bar;
  }, 
  createNew: function(key, options){
    if (!this.bars) this.bars = $H();
    this.bars.set(key, new SimpleProgress(key, options));
  }
};

var QuickForm = Class.create({
  initialize: function(element, options){
    this.options = Object.extend({ draggable: true }, options || {});
    this.element = $(element);
    this.form = this.element.down('form');
    Event.observe((document.onresize ? document : window), "resize", this.handleResize.bindAsEventListener(this, this.element));

    this.handlePositioningChanges(this.element);
    
    if (this.form && this.element.id != 'ticket_form') window.setTimeout(this.form.focusFirstElement.bind(this.form), 750);
    var draggableOptions = { handle: 'title', zIndex: 500 };
    new Draggable(this.element, draggableOptions);
    
    QuickForm.forms.set(this.element.id, this);
    
    SPICEWORKS.fire("app:popup-form:open");

    // we want to hide any active pivot menus when displaying a popup
    PivotManager.clearActive();
    SpiceSelectManager.clearActive();
  },
  handleResize: function(event, element) {
    if (!$(element)) { return; }
    this.handlePositioningChanges(element);
  },
  handlePositioningChanges: function(element) {
    // If the ticket window is really tall and the window is really short (i.e. 1024x768 resolution), change
    // from position:fixed to position:absolute
    element = $(element);
    if ((document.viewport.getHeight() - 15) < $(element).getHeight()) {
      $(element).setStyle("position:absolute;");
      $(element).setStyle("top:" + (document.viewport.getScrollOffsets()[1] + 5) + "px;");
    }
    else {
      $(element).setStyle("position:fixed;");
    }
  },

  destroy: function(){
    Event.stopObserving((document.onresize ? document : window), "resize", this.handleResize.bindAsEventListener(this, this.element));
    this.element.fade({duration:0.5});
    SPICEWORKS.fire("app:popup-form:open");
    
    var that = this;
    setTimeout(function(){
      that.element.remove();
      that.element = that.form = that.options = null;
    }, 500);
  }
});

QuickForm.forms = $H();
QuickForm.close = function(id){
  var form = this.forms.get(id);
  if (form) form.destroy();
  this.forms.unset(id);
};


/* Easily build a popup and display it on the screen.
   var p = Popup.new( 'id', 'Title of the popup', 'Content of the popup' );
   Optional last param of options: {'class':'my_html_class', 'closeable':true}
*/
var Popup = Class.create({
  /* add a popup to the page */
  initialize:function( id, title, popupContent, options ){
    this.options = Object.extend({ closeable: false, draggable: true, 'class':'', 'insert':'document.body', 'immediate':false }, options || {});    

    if( !$(id) ){
      if (this.options.closeable) popupContent += '<p class="btn"><input type="image" src="/images/forms/buttons/small/close.gif" class="image_button" id="' + id + '_close_button" /></p>';
      
      var content = '<div id="' + id + '" class="quick_form ' + this.options['class'] + '" style="' + (this.options['immediate'] ? '' : 'display:none') + '"><div class="inner"><h3 class="title"><a href="#" id="' + id + '_close" class="close" title="Close this window"><img alt="Orange_round_close" src="/images/icons/orange_round_close.png" title="Close this window" /></a><span id="' + id + '_title">' + title + '</span></h3><div id="' + id + '_content" class="content"></div></div></div>';
      
      // If we don't put the popups at the bottom of the document they won't play nice with other floating objects, like help tips, pivots, etc.
      $(document.body).insert({bottom:content});

      Element.update( id + "_content", popupContent);
      if( ! this.options['immediate'] ){
        new Effect.Appear(id,{duration:0.3});
      }

      this.element = $(id);
      $(id + '_close').observe( 'click', this.close.bindAsEventListener(this));
      if(this.options.closeable) $(id + '_close_button').observe( 'click', this.close.bindAsEventListener(this));


      var draggableOptions = { handle: 'title' };

      new Draggable(this.element, draggableOptions);
      Popup.popups.set(this.element.id, this);

      SPICEWORKS.fire("app:popup-form:open");

      PivotManager.clearActive();
      SpiceSelectManager.clearActive();
    }
  },

  /* close and remove the popup from the document */
  close:function(event, close_options){
    if (event) event.stop();
    close_options = close_options || {};

    if(close_options['immediate']){
      this.element.remove();
    }else{
      new Effect.Fade(this.element,{duration:0.3});

      var that = this;
      setTimeout(function() {
        that.element.remove();
      }, 500);
    }

    Popup.popups.unset(this.element.id);

    /* if onclose is defined, then call it as a function here. */
    if( this.options['onclose'] ){
      if( this.options['onclose'] instanceof Function ){
        this.options['onclose']();
      }else{
        eval( this.options['onclose'] );
      }
    }

    // if this was the last popup, then fire
    if (Popup.popups.keys().length == 0) SPICEWORKS.fire("app:popup-form:close");

    return false;
  }
});

Popup.popups = $H();
Popup.close = function( id, close_options ){
  close_options = close_options || {};
  popup = this.popups.get(id);
  if(popup){
    popup.close( null, close_options );
  }
};

var AssetPopup = Class.create();
AssetPopup.prototype = {
  initialize: function( asset ){
    this.asset = asset;
    this.item = $( 'device_' + this.asset );
    this.anchor = this.item.down('a');
    this.hidden = true;
    
    this.listeners = {
      mouseOverItem: this.mouseOverItem.bindAsEventListener( this ),
      mouseOutItem: this.mouseOutItem.bindAsEventListener( this ),
      mouseOverPopup: this.mouseOverPopup.bindAsEventListener( this ),
      mouseOutPopup: this.mouseOutPopup.bindAsEventListener( this ) 
    };

    this.anchor.observe( 'mouseover', this.listeners.mouseOverItem );
    this.anchor.observe( 'mouseout', this.listeners.mouseOutItem );

    this._loadPopup();
  },
  destroy: function(){
    this.anchor.stopObserving( 'mouseover', this.listeners.mouseOverItem );
    this.anchor.stopObserving( 'mouseout', this.listeners.mouseOutItem );
    if (this.popup) {
      this.popup.stopObserving( 'mouseover', this.listeners.mouseOverPopup );
      this.popup.stopObserving( 'mouseout', this.listeners.mouseOutPopup );
      this.popup.remove();    
    } 
    this.popup = null;
  },
  registerPopup: function(){
    // called later, once the popup has been added to the DOM
    this.popup = $( 'device_' + this.asset + '_popup' );

    this.popup.observe( 'mouseover', this.listeners.mouseOverPopup );
    this.popup.observe( 'mouseout', this.listeners.mouseOutPopup );
  },
  mouseOverItem: function( event ){
    // show the popup if it's not already shown, otherwise turn off the hiding flag

    this._stopHiding();

    if ( this.popup && !this.popup.visible() ) this._showPopup();
  },
  mouseOutItem: function( event ){
    // begin the task of hiding the popup, a mouseover on the icon/popup will cancel this task
    this._startHiding();
  },
  mouseOverPopup: function( event ){
    // the popup is already shown if this is happening, make sure we don't hide the popup
    this._stopHiding();
  },
  mouseOutPopup: function( event ){
    // begin the task of hiding the popup, a mouseover on the icon/popup will cancel this task
    this._startHiding();
  },
  popupLoaded: function(){
    // callback after ajax successfully completed, set the flags and show the popup as long as the user hasn't already moved on to something else
    
    this.loaded = true;
    this.registerPopup();
    if (!this.hiding || !this.hidden) this._showPopup();
  },
  _showPopup: function(){
    // draw the popup and set the flags for the current state
    
    this._hideAllPopups(); // only one popup is visible at a time

    var device_position = Position.cumulativeOffset( Browser.ie6 ? this.item : this.anchor );
    var popup_left = device_position[0] - 55;
    var popup_top  = device_position[1] + 70 - $('primary').scrollTop;
    this.popup.setStyle({ top: popup_top + 'px', left: popup_left + 'px' });
    this.popup.show();
    this.hidden = false;
  },
  _startHiding: function(){
    // probably hide the popup, but we need to wait a little bit to give the user a chance to move their cursor from the icon to the popup
    
    this.hiding = window.setTimeout(this._hidePopup.bind(this), 200);
  },
  _stopHiding: function(){
    if ( this.hiding ) window.clearTimeout(this.hiding);
    this.hiding = null;
  },
  _hidePopup: function(){
    // hide the popup
    if ( this.popup && this.popup.visible() ) this.popup.hide();
    this.hidden = true;
  },
  _hideAllPopups: function(){
    $$( 'div.device_popup' ).invoke('hide');
  },
  _loadPopup: function(){
    /* Remove this block to allow popup requests to pile up.  Right now earlier unfinished requests will be cancelled
    in order to start a new one */
    
    if (AssetPopupManager.request) {
      AssetPopupManager.release(AssetPopupManager.request.id); 
      // cancel the previous request so 
      AssetPopupManager.request.ajax.transport.abort();
      AssetPopupManager.request = null;
    }
    AssetPopupManager.request = {};
    AssetPopupManager.request.id = this.asset;
    
    /* end block */
    
    // fetch the popup through Ajax
    AssetPopupManager.request.ajax =  new Ajax.Request('/inventory/fetch_device_popup/' + this.asset, {onSuccess: function() {
      AssetPopupManager.request = null;
    }});
  }
};

var AssetPopupManager = {
  loadForAsset: function(asset){
    // convert to integer
    asset = parseInt( asset, 10 );
    
    if (!this.loadedPopups) this.loadedPopups = $H();

    // create the new popup, but only if we haven't done so already
    // if this popup is already created, then it has it's own registered listeners to show/hide the popup
    if (!this.loadedPopups.get(asset)) this.loadedPopups.set(asset, new AssetPopup(asset));
  },
  loaded: function(asset){
    this.loadedPopups.get(asset).popupLoaded();
  },
  release: function(asset){
    asset = parseInt(asset, 10);
    if ((this.loadedPopups) && (this.loadedPopups.get(asset))){
      this.loadedPopups.get(asset).destroy();
      this.loadedPopups.unset(asset);
    }
  }
};

var CalendarPopup = { 
  setup:function(textFieldID, triggerID, additionalOptions) {
    var calendar;
    additionalOptions = additionalOptions || {};
    // silently fail if the nodes to attach the calendar to cannot be found
    if( $(textFieldID) && (!triggerID || $(triggerID)) ){
      calendar = Calendar.setup({ 
        inputField : textFieldID, // ID of the input field 
        ifFormat : Application.dateFormat, // the date format 
        button : triggerID, // ID of the button
        align : 'Bl',
        single_click : true,
        step : 1, // show every year in menu
        cache : true, // reuse the div is calendar is reopened
        showOthers : true,
        weekNumbers : false,
        onUpdate: additionalOptions.onUpdate,
        getDateStatus: additionalOptions.getDateStatus
      }); 
    }
    return calendar;
  }
};

// Ticket template replacement helper.  Enables clicking on template variable replacements and having them
// added to the form element.
// Taken roughly from: http://alexking.org/blog/2003/06/02/inserting-at-the-cursor-using-javascript/
var template_form_helper = {
  field_name_suffix:null,
  
  // Insert a chunk of text at the cursor of the field.  
  insertAtCursor: function (myValue) {
    if (template_form_helper.field_name_suffix) {
      field_name = $F('selected_template') + this.field_name_suffix;
      field = $(field_name);
    
      //IE support
      if (document.selection) {
        field.focus();
        sel = document.selection.createRange();
        sel.text = myValue;
      }
      //MOZILLA/NETSCAPE support
      else if (field.selectionStart || field.selectionStart == '0') {
        var startPos = field.selectionStart;
        var endPos = field.selectionEnd;
        field.value = field.value.substring(0, startPos)
          + myValue
          + field.value.substring(endPos, field.value.length);
      } else {
        field.value += myValue;
      }
    }
  }
};

var Pivot = Class.create();
Pivot.prototype = {
  initialize: function(activator, menu, options) {
    this.activator = activator;
    this.menu = menu;

    this.options = Object.extend({
      scrollingParent:false,
      onShowCallback: Prototype.emptyFunction,
      onHideCallback: Prototype.emptyFunction
    }, options || {});

    // the options are passed in as eval'd JSON, and therefore if a callback is provided then it will likely be a string instead of a function
    if (typeof this.options.onShowCallback != 'function') this.options.onShowCallback = eval(this.options.onShowCallback);
    if (typeof this.options.onHideCallback != 'function') this.options.onHideCallback = eval(this.options.onHideCallback);
    
    var that = this;
    // add hooks so that we can refer to the menu from the activator and vice-versa, as well as the pivot instance itself
    Object.extend(this.activator, {
      menu: function(){ return that.menu; },
      pivot: function(){ return that; }
    });
    Object.extend(this.menu, {
      activator: function(){ return that.activator; },
      pivot: function(){ return that; }
    });

    this.events = {
      mouseOver: this.mouseOver.bindAsEventListener(this),
      mouseOut:  this.mouseOut.bindAsEventListener(this),
      hasFocus:  this.hasFocus.bindAsEventListener(this),
      lostFocus: this.lostFocus.bindAsEventListener(this),
      menuMouseOver: this.menuMouseOver.bindAsEventListener(this),
      itemMouseOver: this.itemMouseOver.bindAsEventListener(this),
      itemMouseOut: this.itemMouseOut.bindAsEventListener(this)
    };
    this._addObservers();
  },
  mouseOver: function(e) {
    this.clearMenuTimeout();
    this.setActivatorTimeout();
  },
  mouseOut: function(e) {
    this.clearActivatorTimeout();
    this.setMenuTimeout();
  },
  menuMouseOver: function(e) {
    this.clearMenuTimeout();
  },
  itemMouseOver: function(e) {
    this.hideTips();
    this.clearMenuTimeout();
    var element = e.element();
    if (element.prototip) {
      element.prototip.show();
    }
  },
  itemMouseOut: function(e) {
  },
  show: function() {
    this.clearActivatorTimeout();
    this.clearMenuTimeout();
    TipManager.hideAll();  
    
    var dimensions = this.activator.getDimensions(), position;

    // if this pivot is in a scrolling parent, then it is imperative that we 
    // use the viewportOffset in conjunction with the scrollOffset of the body element
    
    // this technique will also work fine for pivots NOT in scrolling parents, but 
    // since it requires an additional offset lookup, it's more expensive so we shouldn't 
    // do it unless it is mandatory
    if (this.options.scrollingParent){
      position = this.activator.viewportOffset();
      var bodyScroll = document.body.cumulativeScrollOffset();
      position.top = position.top + bodyScroll.top;
      position.left = position.left + bodyScroll.left;
    } else {
      position = this.activator.cumulativeOffset();
    }
    
    var menuLeft   = position.left + parseInt(this.activator.getStyle('padding-left'), 10);
    var menuTop    = position.top + dimensions.height;
        
    // if another pivot menu is active, hide it and show this one instead
    if (PivotManager.active && PivotManager.active !== this) PivotManager.active.hide();
    PivotManager.active = this;
    
    if (this.menu && !this.menu.hasClassName('moved_pivot')) {
      document.body.insertBefore(this.menu, $('container'));
      this.menu.addClassName('moved_pivot').setStyle({zIndex:500}).absolutize();
    }
    
    this.menu.setStyle({top: menuTop  + 'px', left: menuLeft + 'px'});
    
    this.menu.show();

    this.menu.observe('mouseover', this.events.hasFocus);
    this.menu.observe('mouseout',  this.events.lostFocus);
    
    if (this.options.onShowCallback) this.options.onShowCallback(this);
    SPICEWORKS.fire("pivot:shown", this.menu);
  },
  
  hide: function() {      
    if (!this.menu) return; // for cases when the pivot menu is no longer on the page due to an ajax process altering the page content
    this.menu.hide();

    this.clearActivatorTimeout();
    this.clearMenuTimeout();

    this.menu.stopObserving('mouseover', this.events.hasFocus);
    this.menu.stopObserving('mouseout',  this.events.lostFocus);
    
    if (this.options.onHideCallback) this.options.onHideCallback(this);
    SPICEWORKS.fire("pivot:hidden", this.menu); 
  },
  
  setActivatorTimeout: function () {
    var timeout  = this.activator.getAttribute('pivot_timeout') || 100;
    this.clearActivatorTimeout();
    this.activatorTimeout = window.setTimeout(this.show.bind(this), timeout);
  },
  
  clearActivatorTimeout: function() {
    if (this.activatorTimeout) window.clearTimeout(this.activatorTimeout);
    this.activatorTimeout = null;
  },

  setMenuTimeout: function() {
    this.clearMenuTimeout();
    this.menuTimeout = window.setTimeout( this.hide.bind( this ), 250 );
  },

  clearMenuTimeout: function() {
    if (this.menuTimeout) window.clearTimeout(this.menuTimeout);
    this.menuTimeout = null;
  },
  
  hasFocus: function(e) {
    SPICEWORKS.fire('pivot:hasFocus', this.activator.id);
    var element = e.element();
    if ( element.hasClassName( 'pivotable' ) || element.up( 'div.pivotable' ) ) this.clearMenuTimeout();
  },
  lostFocus: function(e) {
    if (this.ignoreMouseOut) return;
    var element = e.element();
    if ( !element.hasClassName( 'pivotable' ) || !element.up( 'div.pivotable' ) ) this.setMenuTimeout();
  },
  destroy: function(){
    // if the currently active menu is about to be destroyed, we need to clear/hide it first
    if (PivotManager.active && PivotManager.active == this) PivotManager.clearActive();

    this._removeObservers();
    this.activator.menu = null;
    this.activator.pivot = null;
    this.menu.activator = null;
    this.menu.pivot = null;
    this.activator = null;

    if ( this.menu.parentNode ) this.menu.parentNode.removeChild(this.menu);
    this.menu = null;
  },
  isOrphaned: function(){
    return this.activator.isOrphaned();
  },
  hideTips: function() {
    this.menu.select('a').each ( function(element) {
        if (element.prototip) {          
            element.prototip.hide();
        }
    });
  },
  _addObservers: function() {
    this.activator.observe('mouseover', this.events.mouseOver);
    this.activator.observe('mouseout',  this.events.mouseOut);
    
    this.menu.observe('mouseover', this.events.menuMouseOver);
    
    var that = this;
    this.menu.select('a').each ( function(element) {
      element.observe('mouseover', that.events.itemMouseOver);
      element.observe('mouseout', that.events.itemMouseOut);
    });
  },
  _removeObservers: function(){
    this.activator.stopObserving('mouseover', this.events.mouseOver);
    this.activator.stopObserving('mouseout',  this.events.mouseOut);
    
    this.menu.stopObserving('mouseover', this.events.menuMouseOver);
    var that = this;
    this.menu.select('a').each ( function(element) {
      element.stopObserving('mouseover', that.events.itemMouseOver);
      element.stopObserving('mouseout', that.events.itemMouseOut);
    });
  }
};

var PivotManager = {
  active: null,
  initialize: function(){
    if (this.initialized) return;
    if (!this.pivots) this.pivots = $H();
    this._addNew();
    
    this.listeners = {
      ajaxOnComplete:this.ajaxOnComplete.bindAsEventListener(this),
      clearActive:this.clearActive.bindAsEventListener(this)
    };

    document.observe('ajax:completed', this.listeners.ajaxOnComplete);
    SPICEWORKS.observe('editable-table:start-edit', this.listeners.clearActive);
    SPICEWORKS.observe('reorderable-table:row-moved', this.listeners.clearActive);
    SPICEWORKS.observe('table-row:removed', this.listeners.clearActive);

    // each instance of a pivot has a circular reference that MUST be cleaned up when pages are unloaded
    Event.observe(window, 'unload', this.pageUnload.bindAsEventListener(this));
    this.initialized = true;
  },
  ajaxOnComplete: function(){
    this._removeOrphaned();
    this._addNew();
  },
  clearActive: function(){
    if ( this.active ) this.active.hide();
    this.active = null;
  },
  _removeOrphaned: function(){
    this.pivots.each( function( pair ){
      if ( pair.value.isOrphaned() ){
        pair.value.destroy();
        this.pivots.unset( pair.key );
      }
    }.bind( this ));
  },
  _addNew: function(){
    // pivot elements are usualy anchors, but can be spans as well, so with the selector we don't want to specify the tag name
    $$( '.pivot' ).each( function( pivot ){
      if ( !this.pivots.get( pivot.id ) ){
        var menu = $("menu_" + pivot.id);
        if ( menu ) this.pivots.set( pivot.id, new Pivot( pivot, menu, (pivot.getAttribute('pivot_options') || '{}').evalJSON() ) );
      }
    }.bind( this ));     
  },
  pageUnload: function(){
    this.pivots.each(function(pair){
      // invoke the destroy method for each pivot instance so that it can cleanup after itself
      if (pair.value && pair.value.destroy) pair.value.destroy();
    });
  }
};
Event.register(PivotManager);

var SpiceSelect = Class.create();
SpiceSelect.prototype = Object.extend(new SpiceSelect(), {
  initialize: function( activator, menu, data ) {
    this.activator = activator;
    this.menu = menu;
    this.data = data;
    this.key = this.activator.readAttribute('key'); 
    this.field_id = this.activator.readAttribute('field_id');
    // specifies what hidden form elements will be named
    this.activator.removeAttribute('field_id');
    this.activator.removeAttribute('key');
    
    this.multiSelect = menu.hasClassName('multi');
    this.givenTitle = this.activator.down().innerHTML;
    this._initializeHiddenFields();
    this._updateTitle();
    this.events = {
      activatorMouseDown: this.activatorMouseDown.bindAsEventListener(this), 
      activatorMouseOver: this.activatorMouseOver.bindAsEventListener(this),
      activatorMouseOut:  this.activatorMouseOut.bindAsEventListener(this),
      menuHasFocus:  this.menuHasFocus.bindAsEventListener(this),
      menuLostFocus: this.menuLostFocus.bindAsEventListener(this),
      menuMouseOver: this.menuMouseOver.bindAsEventListener(this),
      itemClick: this.itemClick.bindAsEventListener(this),
      outsideClick: this.outsideClick.bindAsEventListener(this)
    };
    
    this._addObservers();
  },
  uncheckItem: function(element) {
    element.removeClassName('checked');
    element.addClassName('unchecked');
    element.removeAttribute("selected");
    this.data.select("input[key = 'h_" + element.identify() + "']").invoke('remove'); 
  },
  checkItem: function(element) {
    if (this.multiSelect) {
      element.removeClassName('unchecked');
      element.addClassName('checked');
    }
        
    element.writeAttribute("selected", "true");
    var h = this._hiddenField(this.key, element.readAttribute('value'), "h_" + element.identify(), this.field_id);
    this.data.select("input[key = 'h_" + element.identify() + "']").invoke('remove');
    this.data.appendChild(h);
  },
  toggleItem: function(element) {
     (element.hasClassName('checked') ?  this.uncheckItem(element) : this.checkItem(element));
  },
  uncheckAll: function() {
      this.data.update();
      this._checkedItems().each ( function(item) {
        item.removeAttribute('selected');
      });
  },
  itemClick: function(e) {
    var element = e.element();
    
    if (this.multiSelect) {
      this.toggleItem(element);    
      this._updateTitle();
    }
    else {
      this.uncheckAll();
      this.checkItem(element);
      this._updateTitle();
      this.hide();
    }  
  },
  outsideClick: function(e) {
    var element = e.element();
    if (!(element.hasClassName( 'spice_selectable' ) || element.up( 'div.spice_selectable' ))) {
      this.hide();
    }
  },
  activatorMouseDown: function(e) {
    (this.menu.visible() ? this.hide() : this.show());    
  },
  activatorMouseOver: function(e) {
    this.clearActivatorTimeout();
    this.clearMenuTimeout();
  },
  activatorMouseOut: function(e) {
    this.clearActivatorTimeout();
    this.activator.stopObserving('mouseout',  this.events.activatorMouseOut);
    this.menuTimeout = window.setTimeout(this.hide.bind(this), 1000);
  },
  menuMouseOver: function(e) {
    this.clearMenuTimeout();
  },
  menuHasFocus: function(e) {
    var element = e.element();
    if ( element.hasClassName( 'spice_selectable' ) || element.up( 'div.spice_selectable' ) ) this.clearMenuTimeout();
  },
  menuLostFocus: function(e) {
    var element = e.element();
    if ( !element.hasClassName( 'spice_selectable' ) || !element.up( 'div.spice_selectable' ) ) this.menuTimeout = window.setTimeout( this.hide.bind( this ), 1000 );
  },
  show: function() {
    this.clearActivatorTimeout();
    this.clearMenuTimeout();
    
    if (!this.beenShown) {
      // Set the width once. Width is reported as 0 if done in initialize, 
      // and firing it on every show call increases the width each time
      this._setWidth();
      this.beenShown = true;
    }
    
    this._setPosition();
    
    // if another select menu is active, hide it and show this one instead
    if (SpiceSelectManager.active && SpiceSelectManager.active !== this) SpiceSelectManager.active.hide();
    SpiceSelectManager.active = this;
    
    if (this.menu && !this.menu.hasClassName('moved_pivot')) {
      document.body.insertBefore(this.menu, $('container'));
      this.menu.addClassName('moved_pivot');
    }
 
    this.menu.show();
    this.menu.observe('mouseover', this.events.menuHasFocus);
    this.menu.observe('mouseout',  this.events.menuLostFocus);  

    // wait a little bit before watching for an outside click, or else the menu will just flash open and closed
    window.setTimeout( function(){
      document.observe('mousedown', this.events.outsideClick);
    }.bind( this ), 100);    
  },
  
  _setPosition: function() {
    var dimensions = this.activator.getDimensions();
    var position   = this.activator.cumulativeOffset();
    var offset = this.activator.cumulativeScrollOffset();
    // var menuLeft = position.left - this.activator.cumulativeScrollOffset[0] + parseInt(this.activator.getStyle('padding-left'), 10) + document.viewport.getScrollOffsets().left;
    var menuLeft = offset.left + position.left;
    var menuTop = position.top + dimensions.height;
    /* Browser specific pixel-perfect adjustements */
    if (Prototype.Browser.IE) {
      menuTop -= 7;
      if (this.multiSelect) menuTop += 7;
    }
    else if (Prototype.Browser.Gecko) {
      menuTop += -4;
      menuLeft += 1;
      if (this.multiSelect) menuTop += 5;
    }    
    
    var viewHeight = document.viewport.getHeight();
    var menuHeight = this.menu.getHeight();
    var topHidden = document.viewport.getScrollOffsets().top;
    var belowTheFold = (menuTop + menuHeight) - (viewHeight + topHidden);
    
    if (this.multiSelect) {
     // try keep the menu visible on the page
     if (menuHeight > viewHeight) menuTop = topHidden + 5; 
     else if (belowTheFold > 0) menuTop = menuTop - belowTheFold;   
    }
    else {
     // position it so the selected item is right under the cursor
     menuTop= menuTop - (this._checkedCount() > 0 ? this._itemTopOffset(this._checkedItems().first()) : 0);
    } 
    this.menu.setStyle({
      zIndex:   1000,
      position: (this.activator.up("div.quick_form") ? 'fixed' : 'absolute'),
      top:      menuTop  + 'px',
      left:     menuLeft + 'px'
    });        
  },
  
  _setWidth: function() {
    var activatorWidth = this.activator.getWidth();
    var menuWidth = this.menu.getWidth();    

    // at a minimum drop down menu should be the width of the select box
    if ((activatorWidth > menuWidth) || (Browser.ie6)) {
       menuWidth=this.activator.getStyle('width');  // set to the width of the menu, for now. 
       this.menu.setStyle({width: menuWidth});
    }
    else {
        this.menu.setStyle({width: this.menu.getWidth() + 'px'});
    }
  },
  
  hide: function() {
    if (!this.menu) return; // for cases when the pivot menu is no longer on the page due to an ajax process altering the page content
    this.menu.hide();

    this.clearActivatorTimeout();
    this.clearMenuTimeout();

    this.menu.stopObserving('mouseover', this.events.menuHasFocus);
    this.menu.stopObserving('mouseout',  this.events.menuLostFocus);  
    document.stopObserving('mousedown', this.events.outsideClick);
  },
  
  clearActivatorTimeout: function() {
    if (this.activatorTimeout) window.clearTimeout(this.activatorTimeout);
    this.activatorTimeout = null;
  },

  clearMenuTimeout: function() {
    if (this.menuTimeout) window.clearTimeout(this.menuTimeout);
    this.menuTimeout = null;
  },
  destroy: function(){
    // if the currently active menu is about to be destroyed, we need to clear/hide it first
    if (SpiceSelectManager.active && SpiceSelectManager.active == this) SpiceSelectManager.clearActive();

    this._removeObservers();
    this.activator = null;
    if ( this.menu.parentNode ) this.menu.parentNode.removeChild( this.menu );
    this.menu = null;
  },
  isOrphaned: function(){ 
    return this.activator.isOrphaned();
  },
  _updateTitle: function(){   
    if (this.multiSelect) {
      switch (this._checkedCount()) {
        case 0:
          this._setSelectTitle(this.givenTitle);
          break;
        case 1:
           this._setSelectTitle(this._checkedCount() > 0 ? (this._checkedItems().first().innerHTML) : this.givenTitle);
           break;
        default:
           this._setSelectTitle(this._checkedCount() + " selected");
        }
    }
    else {
      if (this._checkedCount() > 0) {
        this._setSelectTitle(this._checkedItems().first().innerHTML);
      }
      else {
        this._setSelectTitle(this.givenTitle);
      }
    }
  },
  _hiddenField: function(name, value, key, id) {
    var h = document.createElement('input');
    h.type="hidden";
    h.value = value;
    h.name = name;
    h.id=id;
    h.setAttribute('key', key);
    return h;
  },
  _selectTitle: function() {
    this.activator.down().innerHTML;
  },
  _setSelectTitle: function(newtitle) {
    this.activator.down().update(newtitle);
  },
  _itemTopOffset: function(element) {
    var id = element.identify();
    var offset = 0;
    var height = 0;
    var topPadding = 0;
    var bottomPadding = 0;
    
    this.menu.select("a.item").each ( function( item ) {
      // calculate using set style since the elements aren't hardcoded with height, and not visible
      topPadding = Number(item.getStyle('padding-bottom').replace('px', ''));
      bottomPadding = Number(item.getStyle('padding-top').replace('px', ''));
      height = Number(item.getStyle('height').replace('px', ''));
      
      offset = offset + height + topPadding + bottomPadding;
      if (id == item.readAttribute('id')) { throw $break; }
    });
    
    this.menu.select("li.separator").each ( function( separator ) {
      topPadding = Number(separator.getStyle('padding-bottom').replace('px', ''));
      bottomPadding = Number(separator.getStyle('padding-top').replace('px', ''));

      offset = offset + topPadding + bottomPadding;
    });
    
    return offset;    
  },
  _initializeHiddenFields: function() {
    this.data.update(); // clear existing hidden elements
    var that = this;
    this._checkedItems().each( function(item) {
      that.checkItem(item);
    });
  },
  _items: function() {
    return this.menu.select("a:not([class~=unselectable])");
  },
  _checkedItems: function() {
    return this.menu.select("a[selected='true']");
  },
  _itemCount: function() {
    return this._items().size();
  },
  _checkedCount: function() {
    return this._checkedItems().size();
  },
  _addObservers: function() {
    this.activator.observe('mousedown', this.events.activatorMouseDown);
    this.activator.observe('mouseover', this.events.activatorMouseOver);
    
    this.menu.observe('mousedown', this.events.menuMouseOver);
    
    var that = this;
    this._items().each( function( item ){
      item.observe('mousedown', that.events.itemClick);
    });
  },
  _removeObservers: function(){
    this.activator.stopObserving('mouseover', this.events.activatorMouseOver);
    this.menu.stopObserving('mouseover', this.events.menuMouseOver);
    
    var that = this;
    this._items().each( function( item ){
      item.stopObserving('mousedown', that.events.itemClick);
    });
  }
});

var SpiceSelectManager = {
  active: null,
  spiceSelects: $H(),
  initialize: function(){
    this._addNew();
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
  },
  refresh: function() {    
    this._removeOrphaned();
    this._addNew();
  },
  ajaxOnComplete: function(){
    this.refresh();
  },
  clearActive: function(){
    if ( this.active ) this.active.hide();
    this.active = null;
  },
  _removeOrphaned: function(){
    if (!this.spiceSelects) { return; }
    this.spiceSelects.each( function( pair ){
      if ( pair.value.isOrphaned() ){
        pair.value.destroy();
        this.spiceSelects.unset( pair.key );
      }
    }.bind( this ));
  },
  _addNew: function(){
    // pivot elements are usualy anchors, but can be spans as well, so with the selector we don't want to specify the tag name
    $$( '.spice_select' ).each( function( activator ){
       if ( !this.spiceSelects.get( activator.id ) ){
         var menu = $("menu_" + activator.id);
         var data = $("data_" + activator.id);
         if ( menu ) this.spiceSelects.set( activator.id, new SpiceSelect( activator, menu, data ) );
       }
     }.bind( this ));  
  },
  get: function(id) {
    var select = this.spiceSelects.get(id);
    return select;
  }
};
Event.register(SpiceSelectManager);

// Disable text selection on an element and all its children.
// Don't do this unless it's temporary (i.e., during a drag operation) or
// something the user doesn't expect to be able to select.
var TextSelection = {
  elements: [],
  initialize: function() {
    this.events = {
      handler: this.handler.bindAsEventListener(this)
    };
    
    Event.observe(window, Prototype.Browser.IE ? 'selectstart' : 'mousedown', this.events.handler);
  },
  
  disable: function(element) {
    element = $(element);
    if (!this.elements.include(element))
      this.elements.push(element);
  },
  
  enable: function(element) {
    element = $(element);
    this.elements = this.elements.without(element);
  },
  
  handler: function(e) {
    var element = e.element();
    do {
      if (this.elements.include(element)) {
        e.stop(); return;
      }
    } while (element = element.parentNode);
  }
};

Event.register(TextSelection);

var Rater = Class.create({
  initialize: function(element, options){
    this.options = Object.extend({ rated: false }, options || {});

    this.rater = $(element);
    this.stars = this.rater.select('a.star');
    
    this.stars.invoke('observe', 'mouseover', this.starMouseOver.bindAsEventListener(this));
    this.stars.invoke('observe', 'mouseout', this.starMouseOut.bindAsEventListener(this));
  },
  starMouseOver: function(event){
    var hovered_star = event.element();
    this.rater.addClassName('hover_at_' + hovered_star.getAttribute('rating'));
  },
  starMouseOut: function(event){
    var hovered_star = event.element();
    this.rater.removeClassName('hover_at_' + hovered_star.getAttribute('rating'));
  }
});

var Flyover = {
  prepare: function( darkbox ){    
    this._setHeights( darkbox );
    SPICEWORKS.fire("app:flyover:shown");
  },
  show: function( flyover_content ){
    if (!$(flyover_content)){
      this._fetchFlyover(flyover_content);
      return;
    }
    var lightbox = $( flyover_content ).up( '.lightbox' );
    var darkbox = $( lightbox.getAttribute('id').replace( 'lightbox_', 'darkbox_' ) );
    lightbox.blindDown( { duration: 1 } );
    darkbox.blindDown( { duration: 1 } );
    window.setTimeout( function(){
      this.prepare( darkbox );
    }.bind( this ), 1100);
    
    SPICEWORKS.fire("app:flyover:shown");
  },
  hide: function( flyover_content ){    
    var lightbox = $( flyover_content ).up( '.lightbox' );
    var darkbox = $( lightbox.getAttribute('id').replace( 'lightbox_', 'darkbox_' ) );
    lightbox.blindUp( { duration: 1 } );
    darkbox.blindUp( { duration: 1 } );
    window.setTimeout( function(){
      if ($(darkbox.id + '_iefix')) {
        $(darkbox.id + '_iefix').remove();
      }
    }.bind( this ), 1100);
    
    SPICEWORKS.fire("app:flyover:hidden");
  },
  destroy: function( element_inside_lightbox, options){
    options = Object.extend( { instantDisplay:false }, options || {} );
    var lightbox = $( element_inside_lightbox ).up( '.lightbox' );
    var darkbox = $( lightbox.getAttribute('id').replace( 'lightbox_', 'darkbox_' ) );
    
    if ( !options.instantDisplay ){
      lightbox.blindUp( { duration: 1 } );
      darkbox.blindUp( { duration: 1 } );
    }
    window.setTimeout( function(){ 
      lightbox.remove();
      darkbox.remove();
    }.bind( this ), ( options.instantDisplay ? 0 : 1100 ) );
    
    SPICEWORKS.fire("app:flyover:hidden");
  },
  _setHeights: function( darkbox ){
    /*
    The height of lightboxes and darkboxes is set in the CSS to be 100% width and height, but in all "good" browsers
    this is rendered as 100% of the width and height of the visible window, and does not include scrollable space
    We need to apply this height fix to make the darkbox and lightbox extend to the full content width
    */
    darkbox = $(darkbox);
    var lightbox = $(darkbox.getAttribute('id').replace('darkbox_', 'lightbox_'));
    
    var height = darkbox.parentNode.offsetHeight;
    if (height && document.documentElement.clientHeight > height) height = document.documentElement.clientHeight;
    darkbox.style.height = height + 'px';
    lightbox.style.height = height + 'px';
  },
  _fetchFlyover: function(flyoverID){
    new Ajax.Request('/utility/flyover/' + flyoverID);
  }
};

var AjaxRequestManager = {
  /* Pass along requestClass: 'foo' in the ajax parameters and previous requests with requestClass 'foo' will get cancelled.
     Also if request 1 = "foo/bar", a request 2 comes in with requestClass "foo", request 1 will be cancelled. */
     
  initialize: function(){
    this.requests = $H();
    this.activeRequests = 0;
    document.observe('ajax:started', this.ajaxStarted.bindAsEventListener(this));
    document.observe('ajax:completed', this.ajaxCompleted.bindAsEventListener(this));
    
    this.stealthRequests = $A(['/tickets/periodic_ticket_table_update', 
    '/finder/application_polling' ]);
    
  },
  ajaxStarted: function(event){    
    var request = event.memo;
    var requestClass = this._getRequestClass(request);
    
    if (requestClass) {
      this._cancelRequestsOfClass(requestClass);
      this._addRequestClass(requestClass, request);
    }
    
    if (!this._requestIsStealth(request.url)) this.incrementRequestCount();
  },
  ajaxCompleted: function(event){
    var request = event.memo;
    var requestClass = this._getRequestClass(request);
    if (requestClass) this._removeRequestClass(requestClass);
    if (!this._requestIsStealth(request.url)) this.decrementRequestCount();
  },
  decrementRequestCount: function() {
    this._modifyRequestCount(-1);
  },
  incrementRequestCount: function() {
    this._modifyRequestCount(1);
  },
  _modifyRequestCount: function(count) {
    this.activeRequests = this.activeRequests + count;
    if (this.activeRequests < 0) this.activeRequests = 0;  
    $(document.body)[(this.activeRequests > 0) ? "addClassName" : "removeClassName"]("busy");
  },
  _getRequestClass: function(request) {
    var requestClass = false;
    if (request.options.requestClass !== undefined) {
      requestClass = request.options.requestClass;
    }
    else if ((request.options.parameters !== undefined) && (request.options.parameters.requestClass !== undefined)) {
      requestClass = request.options.parameters.requestClass; 
    }    
    return requestClass;
  },
  _cancelRequestsOfClass: function(requestClass) {
    var keys = this.requests.keys();
    var exp = new RegExp("^" + requestClass + "\/.+");
    var that = this;
    
    keys.each(function(key) {  
      if ((key == requestClass) || (key.match(exp))) {
        that.requests.get(key).transport.abort();
        that.decrementRequestCount();
      }
    });
  },
  _addRequestClass: function(requestClass, request) {
     this.requests.set(requestClass, request);
  },
  _removeRequestClass: function(requestClass) {
     this.requests.unset(requestClass);
  },
  _requestIsStealth: function(url){ return this.stealthRequests.detect( function(element){ return url.indexOf(element) > -1; } ) ? true : false; }
};

Event.register(AjaxRequestManager);

(function(context){
  var callbacks = {}, builder = function(element, zone, baseURL, parms){
    // use the ID of the element as a unique identifier for the function, but camelize it to remove dashes
    var element = $(element), dynamicFunctionName = element.id.camelize();

    // create a new function as a callback for this techlink, this is an alternative technique to just passing a unique ID as a parameter with the callback
    // we were trying to avoid having to pass both a callback and a unique identifier
    callbacks[dynamicFunctionName] = function(html){ element.update(html); };

    // function to dynamically include the techlink javascript, we will either invoke this function immediately or on dom ready, whichever is more applicable
    var includer = function(){
      var fullURL = new Template("#{base}?cobranded=#{branded}&uuid=#{uuid}&zone=#{zone}&callback=#{callback}#{parms}");
      var replacements = {base:baseURL, branded:Application.cobranded, uuid:Application.uuid, zone:escape(zone), callback:'techLink.callbacks.' + dynamicFunctionName, parms: ( parms == null ? '' : "&" + parms)};
      SPICEWORKS.utils.include(fullURL.evaluate(replacements), function() { /* passing a callback so that our script is removed after it is executed */ });
    };

    // if the dom is already ready, then waiting for the dom loaded event would be a mistake (because it would never fire)
    // otherwise, include the script once the dom is ready so that we're not binding up external load resources on that can block page load times
    if (document.loaded) includer();
    else document.observe('dom:loaded', includer);
  };

  // setup the public object
  window.techLink = {
    builder: builder,
    callbacks: callbacks
  };
})(window);

// webclip is outdated and should be phased out in favor of techlink above which can handle multuple "dynamic scripts" on a given page
// webclip can only handle one per-page, if you call webclip multiple times on a page, only the latest one will be rendered
var WebClip = {
  context:null,
  writeTo:null,
  source:null,
  prepare: function( writeTo, source ){
    this.writeTo = $(writeTo);
    this.source = source;
  },
  deferredReload: function(context){
    document.observe('dom:loaded', this.reload.curry(context).bindAsEventListener(this));
  },
  reload: function(context){
    this.context = context + ',default';
    this.writeTo.update().setStyle( {  } );
    var url = this.source + '?cobranded=' + Application.cobranded + '&uuid=' + Application.uuid + '&what=' + escape(this.context);
    SPICEWORKS.utils.include(url, function(){ /* empty callback will cause the script to be removed on load */});
  },
  render: function(htmlSnippet){
    if ( this.context && this.writeTo && htmlSnippet && htmlSnippet != '' ) this.writeTo.update(htmlSnippet).setStyle({ visibility:'visible' });
  },
  hide: function(){ this.writeTo.setStyle( { } ); }
};

var CollapsableSection = Class.create({
  initialize: function(wrapper){
    this.wrapper = wrapper;
    this.collapsed = this.wrapper.hasClassName('collapsable-collapsed');

    this._appendClassNameHooks();
    this._boundTogglerClicked = this.togglerClicked.bindAsEventListener(this);
    this.toggler = this._setupToggleControl();
    this.toggler.observe('click', this._boundTogglerClicked);
  },
  togglerClicked: function(event){ this.collapsed ? this._expand() : this._contract(); },
  destroy: function(){
    this.toggler.stopObserving('click', this._boundTogglerClicked);
    this.wrapper = this.toggler = this._boundTogglerClicked = null;
  },
  _expand: function(){
    this.wrapper.removeClassName('collapsable-collapsed');
    this.toggler.setAttribute('title', 'Collapse this section');
    this.collapsed = false;
  },
  _contract: function(){
    this.wrapper.addClassName('collapsable-collapsed');
    this.toggler.setAttribute('title', 'Expand this section');
    this.collapsed = true;
  },
  _appendClassNameHooks: function(){
    this.wrapper.select('table thead tr th:first-child').invoke('addClassName', 'first_column');
    this.wrapper.select('table thead tr th:last-child').invoke('addClassName', 'last_column');
  },
  _setupToggleControl: function(){
    var toggler = this.wrapper.down('span.toggler');
    toggler.setAttribute('title', (this.collapsed ? 'Expand this section' : 'Collapse this section'));
    return toggler;
  }
});

var CollapsableSectionManager = {
  initialize: function(){
    this.elements = $H();
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
    this._addNew();
  },
  ajaxOnComplete: function(){
    this._removeOrphaned();
    this._addNew();
  },
  _removeOrphaned: function(){
    this.elements.each(function(pair){
      if (pair.value.wrapper.isOrphaned()){
        pair.value.destroy();
        this.elements.unset(pair.key);
      }
    }.bind(this));
  },
  _addNew: function(){
    $$('div.collapsable').each(function(element){
      if (element.hasClassName('v2')) return;
      if (!element.id) element.setAttribute('id', 'collapsable-' + parseInt(Math.random() * 10000000, 10));
      if (!this.elements.get(element.id)) this.elements.set(element.id, new CollapsableSection(element));
    }.bind(this));
  }
};

(function(context){
  function toggle(link){
    var container = $(link).up('div.collapsable');
    if (container.hasClassName('collapsable-collapsed')) container.removeClassName('collapsable-collapsed');
    else container.addClassName('collapsable-collapsed');
    return false;
  };
  context.collapsable = {
    toggle: toggle
  };
})(window);

Event.register(CollapsableSectionManager);

var ProductRating = {
  writeTo:null,
  source:null,
  prepare: function( writeTo, source ){
    this.writeTo = $( writeTo );
    this.source = source;
    if ( this.writeTo && this.writeTo.hide ) this.writeTo.hide();
  },
  reload: function(){
    if ( this.writeTo && this.writeTo.hide ) this.writeTo.hide().update();
    SPICEWORKS.utils.include(this.source, function(){ /* pass a callback to cause this script to be removed upon loading */ });
  },
  render: function( html_snippet ){
    if ( this.writeTo && html_snippet && html_snippet != '' ) this.writeTo.update( html_snippet ).show();
  }
};

var AdHelper = {
  renderAds: function(url){
    var adsHeight = this.availableHeight();
    if (this.narrowResolution()) Application.narrowLayout();
    else url += '&_w=t';
    
    url += '&_h=' + adsHeight + '&_v=' + escape(SPICEWORKS.version.toString() || '0.0.0');
    url += this.selectedTab(url);
    
    this.adsContainer = $('adbox');
    this.ads = new Element('iframe', {name:'adfame', id:'adframe', src:url, frameborder:0, scrolling:'no', border:0});
    this.ads.setStyle({height: adsHeight + 'px'});
    this.adsContainer.update(this.ads);
    this.setupAdBlockingDetection();
  },
  updateAds: function(url){
    var adsHeight = this.availableHeight();
    if (!Application.narrow) url += '&_w=t';
    url += this.selectedTab(url);
    url += '&_h=' + adsHeight;
    this.ads.src = url;
    this.ads.setStyle({height: adsHeight + 'px'});
  },
  narrowResolution: function(){
    var screenWidth = screen.width;
    return (screenWidth && parseInt(screenWidth, 10) < 1200);
  },
  availableHeight: function(){
    // the height of the ads should be equal to the height of the content wrapper area
    return $('content').getHeight();
  },
  setupAdBlockingDetection: function(){
    if (this.adsContainer){
      this.adCheckingInterval = setInterval((function(adHelper){
        var adframe = $('adframe');
        // if the ads are hidden, show the "Please Try Spiceworks MyWay to get rid of ads" message
        if (adframe && !adframe.visible()) adHelper._displayMyWayImage();
      }).curry(this), 5000);
    }
  },
  selectedTab: function(url){
    var selectedTab = $$$('div#secondary div.sui-tabs li.active'), tab = '';
    if (selectedTab) tab = '&tab=' + escape((selectedTab.id || 'unknown').toLowerCase().replace('_tab', ''));
    return tab;
    
  },
  _displayMyWayImage: function(){
    // clear the polling interval
    if (this.adCheckingInterval) clearInterval(this.adCheckingInterval);

    // show the "myway" ad if we have the container to update
    if (this.adsContainer){
      var myWay = new Template('<a class="myway" href="http://www.spiceworks.com/myway/?aba" target="_blank"><img src="/images/other/spiceworks_myway#{large}.png" alt="Have Spiceworks your way, without ads!" /></a>');
      this.adsContainer.update(myWay.evaluate({large: Application.narrow ? '' : '_300'}));
      // kick off this ajax request so we can do other stuff on the server (record stats)
      new Ajax.Request('/ads/blocking_detected');
    }
  }
};
Event.register(AdHelper);

var EventHelper = {
  toggleAddNew: function(){
    var form = $('add_event_form');
    var toggler = $('link_to_show_new_event');
    if ( form.visible() ){
      form.hide();
      toggler.show();
    } else {
      $('new_event_id').value = '';
      form.show();
      toggler.hide();
      $('new_event_id').focus();
    }
  }
};

var TextHelper = {
  incrementNumberInString: function(element, increment) {
    element = $(element);
    var newCount, match = element.innerHTML.match(/[0-9]+/);
    if (match) {
      newCount = parseInt(match[0], 10) + increment; 
      element.update(element.innerHTML.replace(match[0], newCount));
    }
  },
  showFullText: function( id ){
    var brief = $( 'brief_text_for_' + id );
    var full = $( 'full_text_for_' + id );
    brief.hide();
    full.show();
  }
};

var FlashChart = { diskUsage:null, networkOverview:null, active:null };

var Search = {
  loadCommunityTab: function(query){
    var cookieName = 'community_results_count_' + query;
    var count = Cookie.get(cookieName);
    if(count){
      $('community_results_count').update('(' + count + ')');
      Search.appendResultString.delay(0.5, count, query);
    } else{
      new Ajax.Request(Delegate.encode('/search/app_count'), {
        parameters: {
          'api_version':1,
          'query':query
        },
        onSuccess: function(transport){
          // Save the result in a cookie for 10 minutes to prevent repetitive community requests
          var expireTime = new Date();
          expireTime.setMinutes( expireTime.getMinutes() + 10 );
          Cookie.set(cookieName, transport.responseText, {expires: expireTime});
          var count = transport.responseText || 0;
          $('community_results_count').update('(' + count + ')');
          Search.appendResultString(count, query);
        }
      });
    }
  },
  appendResultString: function(count, query){
    count = parseInt(count || 0, 10);
    var noResult = $$$('div.results').down('li.no_result');
    if (noResult){
      var txt;
      if (count == 0) txt = 'No results found in the Spiceworks Community';
      else txt = '<a href="/search?section=community&query=' + query + '">Found ' + count + ' results in the Spiceworks Community</a>';
      noResult.insert('<br />' + txt);
    }
  },
  loadCommunityResults: function(query, page){
    if (!page) { page = 1; }
    new Ajax.Request(Delegate.encode('/search/app'), {
      parameters: {
        'api_version':1,
        'query':query,
        'page':page
      },
      onSuccess: function(transport){
        $('community_results').update(transport.responseText);
      },
      onFailure: function(transport){
        $('community_results').update('<p>Unable to connect to Community.</p>');
      }
    });
  }
};

/* Helper functions for dealing with attachments to objects. */
var Attachment = {  
  flip_view:function(show_attachment){
    if (show_attachment){
      $('add_description').hide();
      $('add_attachment').show();
      $('attachment_description_body').value = $('description_body').value;
    } else {
      $('add_description').show();
      $('add_attachment').hide();
      $('description_body').value = $('attachment_description_body').value;
      $('attachment_file').value = '';
    }
  }
};


/* Helper functions for dealing with activity streams. */
var Activity = {
  showMenuTimeout: $H(),
  menuTimeout: $H(),
  initialize: function() {
    var defaultResponse = $("activity_prompt").innerHTML.unescapeHTML(); 
    SPICEWORKS.utils.unloader(SPICEWORKS, ["app:ui:secondary:unload", "app:ui:tab:unload"], Activity.unload.bindAsEventListener(this));
    /* Initializes the new activity textarea */
    $$('div.add_activity textarea').each(function (textarea) {
      var div = textarea.up('div.add_activity');
      var meta = div.down('div.meta');
      meta.hide();
  
      textarea.value = defaultResponse;
      textarea.style.color = '#999';
  
      Event.observe(textarea, 'focus', function (event) {
        $(textarea).morphIntoEdit();
        if (textarea.value == defaultResponse) {
          textarea.value = '';
        }
        textarea.style.color = '#111';
        if (!meta.visible()){
          new Effect.SlideDown(meta, {duration:0.3});
        }
      });
  
      Event.observe(textarea, 'blur', function (event) {
        if (textarea.value == '') {
          textarea.style.color = '#999';
          textarea.value = defaultResponse;
        }
      });

      $('add_activity_form').observe('submit', function (event) {
        if (textarea.value == defaultResponse) {
          textarea.value = '';
        }
        return true;
      });
    });
  },
  initializeMenus:function() {
    // Necessary due to bug #14341
    $("activities").observe("click", this.click.bindAsEventListener(this));
    $("activities").observe("mouseover", this.mouseOver.bindAsEventListener(this));
    $("activities").observe("mouseout", this.mouseOut.bindAsEventListener(this));  
  },
  click: function(e) { 
    var el = e.element();
    if (e.findElement('li')) {
      var li = e.findElement('li');
      if (li.hasAttribute('info')) {
        var info = li.getAttribute('info').parseQuery();
        this.toggleDetails(el, info, e);
      }
    }
  },
  unload: function() {
    $$('div.add_activity textarea').each(function(textarea) {
      Event.stopObserving(textarea);
      Event.stopObserving($("add_activity_form"));
    });
    Event.stopObserving("activities");
  },
  mouseOver: function(e) {
    var el = e.element();
    if (el.hasClassName("sw-menu-opener")) {
      this.activatorOver(el.up('li.generic-activity'));
    }
    else if (el.hasClassName("simple-menu") || el.up(".simple-menu")) {
      this.menuOver(el.up('li.generic-activity'));
    }
  },
  mouseOut: function(e) {
    var el = e.element();
    
    if (el.hasClassName("sw-menu-opener")) {
      this.activatorOut(el.up('li.generic-activity'));
    }
    else if (el.hasClassName("simple-menu") || el.up(".simple-menu")) {
      this.menuOut(el.up('li.generic-activity'));
    }
    else if (el.hasClassName("generic-activity")) {
      this.activatorOut(el);
    }
  },
  activatorOver: function(element) {
    var id = element.identify();
    clearTimeout(this.showMenuTimeout.get(id));
    this.showMenuTimeout.set(id, setTimeout(this.menuOpened.bind(this, element), 50));
  },
  activatorOut: function(element) {
    var id = element.identify();
    clearTimeout(this.showMenuTimeout.get(id));
    this.showMenuTimeout.set(id, setTimeout(this.menuClosed.bind(this, element), 100));      
  },
  menuOver: function(element) {
    var id = element.identify();
    clearTimeout(this.showMenuTimeout.get(id));
    clearTimeout(this.menuTimeout.get(id));
  },
  menuOut: function(element) {
    var id = element.identify();
    clearTimeout(this.menuTimeout.get(id));
    this.menuTimeout.set(element.identify(), setTimeout(this.menuClosed.bind(this, element), 100));    
  },
  menuOpened: function(el) {
    el.addClassName('menuOpened');
  },
  menuClosed: function(el) {
    el.removeClassName('menuOpened');
    this.menuTimeout.unset(el.identify());
    this.showMenuTimeout.unset(el.identify());
  },
  hideDetails:function(activator){
    if (activator.tagName != 'LI') activator = $(activator).up('li');
    li.removeClassName('open');
  },
  showDetails:function(activator){
    if (activator.tagName != 'LI') activator = $(activator).up('li');
    li.addClassName('open');
  },
  toggleDetails:function(reference, options, event) {    
    if (event && ((event.target && event.target.tagName == 'A') || (event.srcElement && event.srcElement.tagName == 'A'))) return;
    reference = $(reference);
    var parent = $(reference);
    if (reference.up('li.generic-activity')) {
      parent = reference.up('li.generic-activity');
    }
    
    if (parent.hasClassName('open')) { 
      parent.removeClassName('open');
    }
    else { 
      parent.addClassName('open');
      if (parent.hasClassName("related-activities")) {
        this.loadRelated(parent, options);
      }
    }
  },
  loadRelated: function(parent, options) {
    LoadingMessage.set(parent.down("div.detail"), "Loading…", {'class': 'small'});
    new Ajax.Request('/activities/related/' + options['activity_id'], {method: 'get', parameters:{activity_model: options['activity_model'], activity_model_id:options["activity_model_id"]}, requestClass: ('related-activities-' + options['activity_id']), 
      onSuccess: function() {
        parent.removeClassName("related-activities");
      },
      onFailure: function() {
        StatusMessage.set(parent.down("div.detail"), "An error occurred while trying to load related activities.  Please try again.", {'class': 'small'});
      }});
  },
  showAllDetails:function(){ $$('ul.activities > li:not(.open))').invoke('addClassName', 'open'); },
  hideAllDetails:function(){ $$('ul.activities > li.open').invoke('removeClassName', 'open'); },

  /* Dismiss an item from the activity stream */
  dismiss:function(activator, id, nested){
    this.remove(nested ? $(activator).up('div.nested') : $(activator).up('li'));
    new Ajax.Request('/activities/' + id, { method:'delete' });
  },
  remove:function(activityElement){
    activityElement.fade({duration:0.5});
    Element.remove.delay(0.5, activityElement);
  }
};

var Extension = {
  registered_elements: new Array(),

  register:function( selector, methods ){
    Extension.registered_elements[Extension.registered_elements.length] = [selector, methods];
  },
  attach:function(){
    Extension.registered_elements.each( function(info){
      selector = info[0];
      methods = info[1];
      $$(selector).each( function(elem){
        Object.extend(elem, methods);
      });
    });
  }
};

var PrettyDate = {
  initialize: function() {
    this.pretty();
    document.observe('ajax:completed', this.pretty.bindAsEventListener(this));
  },
  pretty: function() {
    var that = this;
    $$('span.pretty-date').each(function(elem) {
      var prettied = that.format(elem.innerHTML);
      if (prettied) elem.update(prettied);
    });
  },
  /*
   * JavaScript Pretty Date
   * Copyright (c) 2008 John Resig (jquery.com)
   * Licensed under the MIT license.
   */

   // Takes an ISO time and returns a string representing how
   // long ago the date represents.
   format:function(time, options){
     options = options || {isDate:false};
     if (typeof options.isDate == 'undefined') options.isDate = false;
     var date = new Date(options.isDate ? time : (time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
     diff = (((new Date()).getTime() - date.getTime()) / 1000),
     day_diff = Math.floor(diff / 86400);
     if (isNaN(day_diff) || day_diff > 11*30) return;

     return day_diff == 0 && (
       diff < 60 && "just now" ||
       diff < 120 && "1 minute ago" ||
       diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
       diff < 7200 && "1 hour ago" ||
       diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
       day_diff == 1 && "Yesterday" ||
       day_diff < 7 && day_diff + " days ago" ||
       day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
       day_diff < 45 && 'about 1 month ago' ||
       day_diff < (11*30) && 'about ' + Math.ceil(day_diff / 30) + ' months ago' ||
       'about 1 year ago';
   }
};

Event.register(PrettyDate);
// This object is here because the event_data.rxml script was mangling the javascript
// to fetch a device (replacing a + with %20)
var Events = {
  load: function(params) {
    new Ajax.Request('/events', {asynchronous:true, evalScripts:true, parameters:params});
  }
};

/* Manage the ad refreshing for the app
 * Reload when user comes back to the app from another tab/application
 * Watches for window focus to do the reloading.
 * (Wrapped to keep this all out of the global NS)
 */
(function () {
  var timeout = null,
      allowSwap = false,
      swapDelay = 3000, // 3 Seconds before swapping
      showAtLeast = 60000; // 60 Seconds per ad (at least)
  
  // Set Allow Swap to true after some time.
  function startCountdownToSwap() {
    allowSwap = false;
    setTimeout(doAllowSwap, showAtLeast);    
  }
  function doAllowSwap() {
    allowSwap = true;
  }

  function onTabFocus () {
    clearTimeout(timeout);
    timeout = setTimeout(function () {
      if (allowSwap) {
        startCountdownToSwap();
        Application.refreshAd('r');
      }
    }, swapDelay);
  }
  
  // if we leave, just clear the timeout so we don't reload
  function onTabBlur () {
    clearTimeout(timeout);
  }
  
  Event.observe(window, 'focus', onTabFocus);
  Event.observe(window, 'blur', onTabBlur);
  startCountdownToSwap();
})();

var CommunityPopup = {
  defaultOptions:{
    'id':'community_popup',
    'params':{},
    'height':'450',
    'width':'450'
  },
  popup:function(options){
    options = Object.extend(this.defaultOptions, options || {});
    var url = '/community/popup?' + Object.toQueryString(options.params);
    var popupOptions = 'scrollbars=1,height=#{height},width=#{width}'.interpolate({
      'height':options.height,
      'width':options.width
    });
    window.open(url, options.id, popupOptions);
  }
};

var StatusBar = {
  popups: {},
  key: 'status_bar_indicators',
  initialize: function(){
    this.initializeIndicators();
    this.updateCounts();
  },
  initializeIndicators: function() {
    this.popups = {};
    this.values = this.getCachedValues() || {};
    this.popups.plugins = SUI.statusBarPopup($("status-plugins"), {
       title: "Plugin Notifications",
       count: this.values['plugins'],
       onShow: this.callbacks.plugins,
       hideIndicatorOnZero: true,
       beforeShow: this.callbacks.generic.beforeShow, 
       afterShow: this.callbacks.generic.afterShow});

    this.popups.tickets = SUI.statusBarPopup($("status-tickets"), {
       title: "Open Tickets",
       count: this.values['tickets'],
       onCountUpdate: this.callbacks.generic.onCountUpdate.curry({
         positive: {
           singular: "1 New Ticket", 
           plural: "#{count} New Tickets"}}),
       onShow: this.callbacks.tickets,
       afterInitialize: this.callbacks.generic.afterInitialize, 
       beforeShow: this.callbacks.generic.beforeShow, 
       afterShow: this.callbacks.generic.afterShow});
 
       SPICEWORKS.observe("app:helpdesk:ticket:opened", this.popups.tickets.incrementCount.bind(this, 1)); 
       SPICEWORKS.observe("app:helpdesk:ticket:closed", this.popups.tickets.incrementCount.bind(this, -1)); 

    this.popups.errors = SUI.statusBarPopup($("status-scanerrors"), {
       title: "Scan Errors",
       count: this.values['scan_errors'],
       hideIndicatorOnZero: true,
       onCountUpdate: this.callbacks.generic.onCountUpdate.curry({
         positive: {
           singular: "1 New Scan Error", 
           plural: "#{count} New Scan Errors"}}),
       onShow: this.callbacks.errors,
       afterInitialize: this.callbacks.generic.afterInitialize, 
       beforeShow: this.callbacks.generic.beforeShow, 
       afterShow: this.callbacks.generic.afterShow});

       SPICEWORKS.observe("app:scan:error:cleared", this.popups.errors.incrementCount.bind(this, -1));

    this.popups.alerts = SUI.statusBarPopup($("status-alerts"), {
       title: "Active Monitor Alerts",
       count: this.values['alerts'],
       onCountUpdate: this.callbacks.generic.onCountUpdate.curry({
         positive: {
           singular: "1 New Alert", 
           plural: "#{count} New Alerts"}}),
       onShow: this.callbacks.alerts,
       afterInitialize: this.callbacks.generic.afterInitialize, 
       beforeShow: this.callbacks.generic.beforeShow, 
       afterShow: this.callbacks.generic.afterShow});

       SPICEWORKS.observe("app:alert:created", this.popups.alerts.incrementCount.bind(this, 1));
       SPICEWORKS.observe("app:alert:cleared", this.popups.alerts.incrementCount.bind(this, -1));

    this.popups.offline = SUI.statusBarPopup($("status-offline"), {
       title: "Offline Devices",
       count: this.values['offline'],
       hideIndicatorOnZero: true,
       onCountUpdate: this.callbacks.generic.onCountUpdate.curry({
         positive: {
           singular: "1 Device Went Offline", 
           plural: "#{count} Devices Went Offline"},
         negative: {
           singular: "1 Device Came Online",
           plural: "#{count} Devices Came Online"
         }}),
       onShow: this.callbacks.offline,
       beforeShow: this.callbacks.generic.beforeShow, 
       afterShow: this.callbacks.generic.afterShow});

     this.popups.messages = SUI.statusBarPopup($("status-messages"), {
        title: "Private Messages",
        count: this.values['messages'],
        hideCountOnZero: true,
        afterInitialize: this.callbacks.messages.afterInitialize,
        onShow: this.callbacks.messages.onShow,
        onCountUpdate: this.callbacks.messages.onCountUpdate});

     this.popups.community =  SUI.statusBarPopup($("status-community"), {
        title: "Community Activity",
        hideCountOnZero: true,
        count: this.values['community'],
        beforeShow: this.callbacks.community.beforeShow,
        afterInitialize: this.callbacks.community.afterInitialize,
        onCountUpdate: this.callbacks.community.onCountUpdate,
        onShow: this.callbacks.community.onShow});
        
      this.popups.about = SPICEWORKS.ui.simplemenu($('indicator-app-about'), $('app-about-popup'), {
        align:function(menu, activator){} // Skip the align callback
      });
  },
  setCounts:function(json) {
    this.popups.alerts.setCount(json['alerts']);
    this.popups.tickets.setCount(json['tickets']);
    this.popups.offline.setCount(json['offline']);
    this.popups.errors.setCount(json['scan_errors']);
    this.popups.plugins.setCount(json['plugins']);
    
    this.popups.messages.setCount(json['messages']);
    
    this.popups.community.setCount(json['community']);
    this.popups.community.setData('lastSeen', new Date(json['community_timestamp']));
  },
  updateCounts: function(json) {
    if (!json) {
      json = this.getCachedValues();
      this.setCounts(json);
      
      //request new data
      new Ajax.Request("/utility/statistics", {method: 'get'});      
    }
    else {
      this.values = json;
      this.setCounts(json);
      this.cacheValues(json);
    }
  },
  callbacks: {
    generic: {
      beforeShow: function(self) {
         self.setData('lastSeen', self.getData('seen'));
         if (!self.getData('data')) {
           self.resetLayout("Loading…");
           self.clearFlag('unseen');
         }
      },
      afterInitialize: function(self) {
        // So any new items that come in after page load will be marked as new
        self.setData('seen', new Date());
      },
      afterShow: function(self) {
        self.setData('seen', new Date());
      },
      onCountUpdate: function(dict, self, oldCount, newCount) {
        var diff = self.setData('diffCount', ((self.getData('diffCount') || 0) + newCount - oldCount));        
        var text, message;
        
        if ((diff == 1) && (dict['positive'])) { text = dict['positive']['singular']; }
        else if ((diff > 1) && (dict['positive'])) { text = dict['positive']['plural']; }
        else if ((diff == -1) && (dict['negative'])) { text = dict['negative']['singular']; }
        else if ((diff < -1) && (dict['negative'])) { text = dict['negative']['plural']; }
        
        self.clearData('data');
        
        if (diff > 0) {
          self.setFlag('unseen');
        }
        if (self.getData('predictedCount') != newCount) {
          if (self.elements('panel') && self.elements('panel').visible()) {
            message = new Element('span');
            message.insert("Data has recently changed. " + "  ");
            message.insert(self.elements('refreshLink').update("Refresh Data"));
            self.setAlertMessage(message);
          }
          if (text) {
            text = text.interpolate({count: Math.abs(diff)});
            self.notify(text);
          }
        }
      }
    },
    community: {
      beforeShow: function(self) {
        // If data isn't set, it means that the count changed since the last time we refreshed
        if (!self.getData('data')) {
           self.resetLayout("Loading…");
           self.clearFlag('unseen');
        }
      },
      afterInitialize: function(self) {
        function updateCount(data) {
          var lastSeen = self.getData('lastSeen');
          var count = data['timestamps'].select(function(time) {
            return (lastSeen ? ((new Date(time)) > (new Date(lastSeen))) : true);
          }).size();

          if (self.getCount() != count) {
            new Ajax.Request('/settings/users/update.json', {method: 'post', parameters: {
                 "id": SPICEWORKS.app.user.id,
                 "user[community_activity_count]": count,
                 "user[community_activity_seen_at]": data['timestamps'][0]
            }});
          }
          self.setCount(count); 
        }
        
        function refresh() {
          SPICEWORKS.community.activity.userTimestamps({}, updateCount, "status_bar");
        }
        
        refresh(); // Do a refresh 
        SPICEWORKS.utils.focusTimer(60, false, refresh);
      },
      onCountUpdate: function(self, oldCount, newCount) {
        var diff = self.setData('diffCount', ((self.getData('diffCount') || 0) + newCount - oldCount));        
        var text, message;
        
        if (diff == 1)  { text = "1 New Activities"; }
        else if (diff > 1) { text = "#{diff} New Activities".interpolate({diff: diff}); }
        self.clearData('data');
        
        if (diff > 0) {
          self.setFlag('unseen');
          
          if (self.elements('panel') && self.elements('panel').visible()) {
            message = new Element('span');
            message.insert("Data has recently changed. " + "  ");
            message.insert(self.elements('refreshLink').update("Refresh Data"));
            self.setAlertMessage(message);
          }
          if (text) {
            text = text.interpolate({count: Math.abs(diff)});
            self.notify(text);
          }
        }
      },
      onShow: function(self) {
        function displayData(data){
          self.setFooter(SPICEWORKS.community.linkTo(data['path'], "status_bar", "activity").update("View Community Activity"));
          if (data['activities'].size() > 0) {
              var formatted = data['activities'].inject([], function(memo, value, index) {
                memo.push({
                  title: value['title'],
                  subtitle: value['subtitle'],
                  icon: SPICEWORKS.community.url(value['icon']['20px']['path']),
                  message: SUI.truncate(value['message'], 150),
                  date: value['timestamp'],
                  when: self.formatters.date(value['timestamp']),
                  type: 'activity',
                  communityUrl: SPICEWORKS.community.trackingPath(value['path'], "status_bar", "activity"),
                  unseen: (self.getData('lastSeen') && (self.getData('lastSeen') < new Date(value['timestamp'])))
                });
                return memo;
              });
              self.update(self.builders.list(formatted));
            
              var lastSeen = new Date(self.getData('lastSeen')).getTime();
              var latestStamp = new Date(data['activities'][0]['timestamp']).getTime();            
              if ((lastSeen != latestStamp) || (self.getCount() != 0)) {
                 new Ajax.Request('/settings/users/update.json', {method: 'post', parameters: {
                      "id": SPICEWORKS.app.user.id,
                      "user[community_activity_count]": "0",
                      "user[community_activity_seen_at]": data['activities'][0]['timestamp']
                 }});
              }
              self.setCount(0);
          }
          else {
            self.setPanelMessage("No Community Activity");
          }
        }

        function response(data) {
          if (!data['error']) {
            self.setData('data', data);
            displayData(data);
          }
          else {
            self.setPanelMessage("An Error Occurred, Please Try Again Later.");
          }
        }
        
        if (self.getData('data')) { displayData(self.getData('data')); }
        else { SPICEWORKS.community.activity.user({}, response, "status_bar"); }
      }
    },
    messages: {
      afterInitialize: function(self) {
        function updateCount(data) {
          if (self.getCount() != data['unread_count']) {
            new Ajax.Request('/settings/users/update.json', {method: 'post', parameters: {
              id: SPICEWORKS.app.user.id,
              "user[community_unread_message_count]": data['unread_count']
            }});
          }
          self.setCount(data['unread_count']);
        }
        function refresh() {
          SPICEWORKS.community.messages.unreadCount({}, updateCount, "status_bar");
        }
        refresh();
        SPICEWORKS.utils.focusTimer(60, false, refresh);
      },
      onCountUpdate: function(self, oldCount, newCount) {
        var diff = self.setData('diffCount', ((self.getData('diffCount') || 0) + newCount - oldCount));        
        var text, message;
        
        $("header_inbox_link").update("Inbox#{count}".interpolate({
          count: (newCount > 0 ? (" (" + newCount + ")"): "")
        }));
        
        if (diff == 1)  { text = "1 New Messages"; }
        else if (diff > 1) { text = "#{diff} New Messages".interpolate({diff: diff}); }
        self.clearData('data');
        
        if (diff > 0) {
          self.setFlag('unseen');
          
          if (self.elements('panel') && self.elements('panel').visible()) {
            message = new Element('span');
            message.insert("Data has recently changed. " + "  ");
            message.insert(self.elements('refreshLink').update("Refresh Data"));
            self.setAlertMessage(message);
          }
          if (text) {
            text = text.interpolate({count: Math.abs(diff)});
            self.notify(text);
          }
        }
      },
      onShow: function(self) { 
        self.setFooter(SPICEWORKS.community.linkTo('/messages/inbox', "status_bar", "pm").update("View Inbox"));
        function response(data) {
          if (!data['error']) {
            self.setData('data', data);
            displayData(data);
          }
          else {
            self.setPanelMessage("An Error Occurred, Please Try Again Later.");
          }
        }
        
        function displayData(data){ 
          if (data['messages'].size() > 0) {  
            var formatted = data['messages'].inject([], function(memo, value, index) {
              memo.push({
                title: new Element('span', {style: "color:#{color}".interpolate({color:value['from']['color']})}).update(value['from']['name']),
                subtitle: value['subject'],
                icon: SPICEWORKS.community.url(value['from']['avatars']['20px']['path']),
                message: SUI.truncate(value['text'], 150),
                date: value['last_post_created_at'],
                when: self.formatters.date(value['last_post_created_at']),
                type: 'message ' + (value['unread'] ? "unread": ""),
                communityUrl: SPICEWORKS.community.trackingPath(value['path'], "status_bar", "pm"),
                unseen: (self.getData('lastSeen') && (self.getData('lastSeen') < new Date(value['last_post_created_at'])))
              });
              return memo;
            });
            self.update(self.builders.list(formatted));
          }
          else {
            self.setPanelMessage("No Messages");
          }
        };
        if (self.getData('data')) { displayData(self.getData('data')); }
        else { SPICEWORKS.community.messages.inbox({}, response, "status_bar"); }
      }
    },
    tickets: function(self) {
      self.setFooter(SPICEWORKS.app.linkTo('/tickets/list/open_tickets').update("View Tickets"));

      function displayData(data) {
        if (data.size() > 0) {
          var formatted = data.inject([], function(memo, value, index) {
            memo.push({
              title: SPICEWORKS.app.linkTo(value['ticket_url'], {dispatch_info: $H({id:value['id'], model: "Ticket"}).toQueryString()}).update('#' + value['id'] + ": " + value['summary']).toHTML(),
              message: SUI.truncate(value['description'], 150),
              date: value['created_at'],
              when: self.formatters.date(value['created_at']),
              filter: ((value['assignee'] && SPICEWORKS.app.user.id == value['assignee']['id']) ? "mine" : ""),
              type: 'ticket',
              unseen: (self.getData('lastSeen') && (self.getData('lastSeen') < new Date(value['created_at']))) 
            });
            return memo;
          });
          self.update(self.builders.list(formatted));
        }
        else {
          self.setPanelMessage("No Open Tickets");
        }
      }
      
      function refreshData() {
        new Ajax.Request("/api/tickets.json", {
          method: 'get', parameters: {filter: 'open'},
          onSuccess: function(transport) {       
            self.setData('data', transport.responseText.evalJSON());
            displayData(self.getData('data'));
          },
          onFailure: function(transport) {
            self.setMessage("Something Went Horribly Wrong");
          }
        });
      }
      
      if (self.getData('data')) { displayData(self.getData('data')); }
      else { refreshData(); }
    },
    alerts: function(self) {
      var list, footer, clearLink, selectedCount = 0;

      function clearSelectedAlerts(event) {
        var queryString = Form.serialize(list);
      }
    
      function onDismiss(li) {
        var id = li.getAttribute('id').match("[0-9]*$");
        if (id) { id = id[0]; }
        new Ajax.Request('/alerts/destroy', {method: 'post', parameters: {id: id},
          onSuccess:function(transport) {
            self.setData('predictedCount', (self.getData('predictedCount') || self.count) - 1);
            li.fade({duration: 0.5, afterFinishInternal: function() {  li.remove(); }});
          },
          onFailure:function(transport) {
            self.setAlertMessage("An Error Occurred While Trying To Clear Alert");
          }});
      }
    
      function setupBulkActions(count) {
        if (!clearLink) {
          clearLink = new Element('a', {href: SUI.voidLink}).observe('click', clearSelectedAlerts);
        }
        if (selectedCount > 0) {
          self.setActionBox(clearLink.update("Clear #{count} Alert#{s}".interpolate({count:selectedCount, s:(selectedCount > 1 ? "s" : "") } )));
        }
        else {
          self.hideActionBox();
        }
      }
      function onCheck(li) {
        selectedCount = selectedCount + 1;
        setupBulkActions(selectedCount);
      }
      function onUncheck(li) {
        selectedCount = selectedCount - 1;
        setupBulkActions(selectedCount);
      }
    
      function selectAllOrNone(event) {
        var el = event.element();
        var inputs = self.elements('canvas').select("input[name=to_delete]");
        if (!el.checked) {
          inputs.each(function(checkbox){ checkbox.checked = false; });
          self.elements('canvas').select('li').invoke("removeClassName", "selected");
          selectedCount = 0;
        }
        else {
          inputs.each(function(checkbox){ checkbox.checked = true; });
          self.elements('canvas').select('li').invoke("addClassName", "selected");
          selectedCount = inputs.size();
        }
        setupBulkActions(selectedCount);
      }
    
      function setFooter(count) {
        var link = SPICEWORKS.app.linkTo("/inventory/?summary=alert_summary").update("View Alerts");
        if (count && count > 0) {
          var span = new Element('span', {'style':'float:left;'});      
          span.insert(new Element('input', {'type': 'checkbox', 'id':'alert_select_all_none'}).observe("click", selectAllOrNone));
          span.insert(new Element('label', {'for': 'alert_select_all_none'}).update("Select All/None"));
          self.setFooter(span);
          span.insert({after:link});
        }
        else {
          self.setFooter(link);
        }
      }
    
      function displayData(data) {
        if (data.size() > 0) {
          var title, link, message;
          var formatted = data.inject([], function(memo, value, index) {
            if (value['alertable_type'] == "Device") {
              link = SPICEWORKS.app.linkTo(value['alertable_url'],{dispatch_info: $H({id:value['alertable_id'], model: value['alertable_type'], tab: 'general'}).toQueryString()}).update(value['alertable']['name']);
              message = value['message'].replace(value['alertable']['name'], "");
              
              if (value['alertable']['name'] != value['title']) {
                title = link.toHTML() + ": " +  value['title'] + " " + message;
              }
              else {
                title = link.toHTML() + " " + message;                
              }              
            }
            else {
              link = SPICEWORKS.app.linkTo(value['alertable_url'],{dispatch_info: $H({id:value['alertable_id'], model: value['alertable_type'], tab: 'general'}).toQueryString()}).update(value['title']);
              title = link.toHTML() + " " + value['message'];
            }
                      
             memo.push({
               id: 'status_list_alert_' + value['id'],
               title: title,
               icon: '/images/icons/small/' + value['icon'] + '.png',
               type: 'alert',
               // selectable: {
               //   value: value['id'],
               //   name: 'to_delete'}, 
               when: self.formatters.date(value['updated_at']),
               dismissable: {
                 title: "Clear this alert"
               },
               date: value['updated_at'],
               unseen: (self.getData('lastSeen') && (self.getData('lastSeen') < new Date(value['updated_at'])))  }
             );
             return memo;
           });
           list = self.builders.list(formatted, {
             'onCheck': onCheck, 
             'onUncheck': onUncheck, 
             'onDismiss': onDismiss});
           self.update(list);
            // setFooter(list.select("li").size());
        }
        else {
          self.setPanelMessage("No Active Alerts");
        }
      }
    
      function refreshData() { 
        new Ajax.Request("/api/alerts.json", {
           method: 'get', parameters: {filter: 'active'},
           onSuccess: function(transport) {
             self.setData('data', transport.responseText.evalJSON());
             displayData(self.getData('data'));
           },
           onFailure: function(transport) {
             self.setMessage("Something Went Horribly Wrong");
           }
        });
      }
          
      setFooter();
      if (self.getData('data')) { displayData(self.getData('data')); }
      else { refreshData(); }
    },
    errors: function(self) {
      function refreshData() {
        new Ajax.Request("/api/alerts.json", {
           method: 'get', parameters: {filter: 'errors'},
           onSuccess: function(transport) {
             self.setData('data', transport.responseText.evalJSON());
             displayData(self.getData('data'));
           },
           onFailure: function(transport) {
             self.setMessage("Something Went Horribly Wrong");
           }
        });
      }
      
      function displayData(data) {
        if (data.size() > 0) {
          var link, message, icon, unknown, type;
          var formatted = data.inject([], function(memo, value, index) {
             link = SPICEWORKS.app.linkTo(value['alertable_url'],{dispatch_info: $H({id:value['alertable_id'], model:value['alertable_type'], tab:'alerts'}).toQueryString()}).update(value['alertable']['name']);
           
             unknown = (value['alertable']['device_type'] == "Unknown");

             if (unknown) {
               icon = '/images/icons/statusbar/unknowns.png';
             }
             else {
               icon = '/images/icons/small/' + value['icon'] + '.png';
             }
             
             if (value['alert_type'] == "RackspaceScanError") {
               message = value['message']['friendly'];
               type = "rackspace";
             }
             else if (value['alert_type'] == "WarrantyScanError") {
               message = value["message"];
               type = "warranty";
             }
             else {
               message = "";
               type = "";
             }
             
           
             memo.push({
               title: link.toHTML(),
               subtitle: value['title'],
               message: message,
               icon: icon, 
               filter:  (unknown ? "unknown" : ""),
               type: 'scan_alert ' + (unknown ? "unknown" : "known"),
               when: self.formatters.date(value['updated_at']),
               date: value['updated_at'] }
             );
             return memo;
           });

           self.update(self.builders.list(formatted));
         }
         else {
           self.setPanelMessage("No Scan Errors");
         }
      }
      
      self.setFooter(SPICEWORKS.app.linkTo("/inventory/?summary=errors").update("View Scan Errors"));
      if (self.getData('data')) { displayData(self.getData('data')); }
      else { refreshData(); }
    },
    offline: function(self) {
      function refreshData() { 
        new Ajax.Request("/api/devices.json", {
           method: 'get', parameters: {category: 'offline', sort_by: 'offline_at', reverse: 'true'},
           onSuccess: function(transport) {
             self.setData('data', transport.responseText.evalJSON());
             displayData(self.getData('data'));
           },
           onFailure: function(transport) {
             self.setMessage("Something Went Horribly Wrong");
           }
        });
      }
      
      function displayData(data) {
        if (data.size() > 0) {
          var link, message;
          var formatted = data.inject([], function(memo, value, index) {
             link = SPICEWORKS.app.linkTo(value['device_url'], {dispatch_info: $H({id:value['id'], model:"Device"}).toQueryString()}).update(value['name']);
           
             message = [];
             if (value['primary_owner_name']) { message.push(value['primary_owner_name']); }
             message.push(value['ip_address']);
             memo.push({
               title: link.toHTML(),
               message: message.join(" / "),
               icon:'/images/icons/small/' + value['icon'] + ".png", 
               type: 'offline',
               date: value['offline_at'],
               when: "went offline " + (!value['offline_at'] ? "" : self.formatters.date(value['offline_at'])) }
             );
             return memo;
           });
           self.update(self.builders.list(formatted));
         }
         else {
           self.setPanelMessage("No Devices Are Offline");
         }
      }
      
      self.setFooter(SPICEWORKS.app.linkTo("/inventory/offline").update("View Offline Devices"));     
      if (self.getData('data')) { displayData(self.getData('data')); }
      else { refreshData(); }
    },
    plugins: function(self) {
      self.setFooter(SPICEWORKS.app.linkTo("/settings/plugins").update("View Plugins"));
      
      new Ajax.Request("/settings/plugins/notifications.json", {
        parameters: {method: 'get'},
        onSuccess: function(transport) {
          var link, message, data = transport.responseText.evalJSON();           
          if (data.size() > 0) {
            var formatted = data.inject([], function(memo, value, index) {
              link = SPICEWORKS.community.linkTo("/plugin/guid/" + value['guid'], "status_bar", "plugin").update(value['name']);
              memo.push({
                title: link.toHTML() + " has an update available.", 
                type: 'update' }
              );
              return memo;             
            });
            self.update(self.builders.list(formatted));
          }
          else {
            self.setPanelMessage("No Plugin Notifications");
          }
         },
         onFailure: function(transport) {
           self.setMessage("Something Went Horribly Wrong");
         }
      });
    }
  },
  cacheValues: function(json) {
    var currentValues = this.getCachedValues();
    Cookie.set(this.key, Object.toJSON(Object.extend(currentValues, json)), {path: '/'});
  },
  getCachedValues: function() {
    return unescape(Cookie.get(this.key) || "{}").evalJSON();
  }
};

CommunityProductData = {
  response: null,
  initialize: function(){
    $$('.product_rater').each(function(element){
      new Rater(element);
    });
  },
  callback: function(product){
    var wrapper = $('community_device_info_wrapper');
    this.response = product;
    if(wrapper && !product.not_found){
      this.draw(wrapper, product);
    } else {
      this.drawNotFound(wrapper, product);
    }
  },
  drawNotFound: function(wrapper, product){
    if(wrapper && wrapper.down('div.product')){
      wrapper.down('div.product').addClassName('product_full_width');
      if(product.vendor && product.vendor.name){
        wrapper.down('h4.product_name span.vendor').update(product.vendor.name.escapeHTML());
      }
      if(product.model){
        wrapper.down('h4.product_name span.model').update(product.model.escapeHTML());
      }
      this._drawProductLinks(wrapper, product);
    }
  },
  draw: function(wrapper, product){
    if(product.model){
      wrapper.down('h4.product_name span.model').update(product.model.escapeHTML());
    }
    if(product.avatars && product.avatars['60px'] && product.avatars['60px'].path){
      wrapper.down('div.product_image').setStyle({
        'backgroundImage':'url(#{url})'.interpolate({
          url: SPICEWORKS.community.url( product.avatars['60px'].path )
        })
      });
    }
    if(product.id){
      var productLink = SPICEWORKS.community.linkTo('/product/' + product.id);
      productLink.title = 'Learn more about this product in the Spiceworks Community';
      wrapper.down('div.product_image').insert(productLink.clone().insert('&nbsp;'));
      var productName = wrapper.down('h4.product_name');
      productName.update(productLink.clone().update(productName.innerHTML));
    }
    if(product.avg_rating){
      if(product.times_rated){
        wrapper.down('p.rating span.rating_count').update( '(' + product.times_rated + ')' );
      }
      this.updateRatingClass(product.avg_rating);
      wrapper.down('p.rating').show();
    }
    this._drawProductLinks(wrapper, product);
    if(product.vendor){
      if(product.vendor.name && !product['is_service?']){
        wrapper.down('h4.product_name span.vendor').update(product.vendor.name.escapeHTML());
      }
      if(product.vendor.group && product.vendor.group.name && product.vendor.group.to_param){
        var groupLink = SPICEWORKS.community.linkTo('/group/show/' + product.vendor.group.to_param);
        groupLink.title = 'Visit the ' + product.vendor.group.name.escapeHTML() + ' group in the Spiceworks Community';
        wrapper.down('h4.group_header span.group_name').update(
          groupLink.clone().update(
            product.vendor.group.name.escapeHTML()
          )
        );
        if(product.vendor.group.avatars && product.vendor.group.avatars['20px'] && product.vendor.group.avatars['20px'].path){
          wrapper.down('h4.group_header').setStyle({
            'backgroundImage':'url(#{url})'.interpolate({
              url: SPICEWORKS.community.url( product.vendor.group.avatars['20px'].path )
            })
          });
        }
        if(product.vendor.group.group_forum && product.vendor.group.group_forum.latest_topics){
          var groupTopics = wrapper.down('ul.group_topics');
          product.vendor.group.group_forum.latest_topics.each(function(topic) {
            groupTopics.insert(
              new Element('li').update(
                SPICEWORKS.community.linkTo('/topic/' + topic.id).update( 
                  topic.subject.escapeHTML()
                )
              )
            );
          });
          groupTopics.show();
        }
        wrapper.down('div.group').show();
      } else {
        wrapper.down('div.product').addClassName('product_full_width');
      }
    }
    wrapper.show();
  },
  rate: function(params){
    var that = this;
    new Ajax.Request('community:/api/products/rate.json', {
      parameters:params,
      onLoading: function(response){
        that.updateRatingClass(params.rating);
        that.showRatingSpinner();
      },
      onComplete: function(response){
        that.hideRatingSpinner();
      },
      onSuccess: function(response){
        var wrapper = $('community_device_info_wrapper');
        var json = response.responseJSON;
        if(json && json.success){
          if(json.your_rating){
            that.updateRatingClass(json.your_rating);
          }
          if(json.times_rated){
            wrapper.down('p.rating span.rating_count').update( '(' + json.times_rated + ')' );
          }
          that.updateRatingMessage(json.message || 'Thanks!');
        } else {
          that.updateRatingMessage('Error');
        }
      },
      onFailure: function(response){
        var wrapper = $('community_device_info_wrapper');
        var json = response.responseJSON;
        if(json){
          that.updateRatingMessage(json.message || 'Error');
        } else {
          that.updateRatingMessage('Error');
        }
      }
    });
  },
  updateRatingMessage: function(message){
    var messageElement = $('community_device_info_wrapper').down('span.message');
    messageElement.update(message);
    setTimeout(function() { messageElement.update(); }, 3000);
  },
  updateRatingClass: function(rating){
    var rater =  $('community_device_info_wrapper').down('p.rating span.product_rater');
    $w(rater.className).select(function(className){ 
      return className.startsWith('rated_at_');
    }).each(function(className){
      rater.removeClassName(className);
    });
    rater.addClassName(this._ratingClass(rating));
  },
  showRatingSpinner: function(){
    $('community_device_info_wrapper').down('p.rating span.spinner').show();
  },
  hideRatingSpinner: function(){
    $('community_device_info_wrapper').down('p.rating span.spinner').hide();
  },
  _drawProductLinks: function(wrapper, product){
    if(product.links){
      product.links.each(function(link){
        var linkElement = new Element('li');
        if(link.path){ 
          linkElement.update(
            SPICEWORKS.community.linkTo(link.path).update(
              link.text.escapeHTML()
            )
          );
        } else if(link.url) {
          linkElement.update(
            new Element('a', {'href': link.url}).update(
              link.text.escapeHTML()
            )
          );
        }
        wrapper.down('div.product_links ul').insert(linkElement);
      });
    }
  },
  _ratingClass: function(rating){
    // round the rating to the nearest 0.5 and build a class name
    var ratingTimes2, ratingInteger, ratingDecimal, ratingClass;
    if(!rating){
      return 'rated_at_0';
    }
    if(rating > 5){ return 'rated_at_5'; }
    if(rating < 1){ return 'rated_at_1'; }
    ratingTimes2 = (rating * 2).round();
    ratingInteger = (ratingTimes2 / 2).floor();
    ratingDecimal = ratingTimes2 % 2;
    ratingClass = 'rated_at_' + ratingInteger;
    if(ratingDecimal == 1){
      ratingClass += '_5';
    }
    return ratingClass;
  }
};

(function(context){
  var register = function(){
    new Draggable($$('div.intro-to-page').first(), {handle:'intro-heading'});
    $$('div.intro-to-page h3 a.intro-close').invoke('observe', 'click', function(){ $$('div.intro-to-page').invoke('hide'); });
  };
  var show = function(){
    var intro = $$$('div.intro-to-page');
    if (intro && !intro.visible()) intro.show();
  };

  context.PageIntro = {
    register:register,
    show:show
  };
})(window);

(function(context){
  var play = function(url, container, options){
    
    // If Flash isn't installed, swfobject won't replace container, and the message below will be shown.
     $(container).update("<div class='no_flash'><p>Flash 10.0 is required to view this video.</p><p><a href='http://www.adobe.com/go/getflashplayer'><img src='/images/other/get_flash_player.png'></img></a></p></div>"); 
    
    options = options || {};
    options.width = options.width || '100%';
    options.height = options.height || '100%';
    options.extraArgs = options.extraArgs || {};
    swfobject.embedSWF(url, container, options.width, options.height, "10.0.0", "/flash/expressinstall.swf", options.extraArgs, {wmode: 'opaque'});
  };
  context.Video = {
    play:play
  };
})(window);

/**
 * Helper function to put a page into "print view"
 * Not totally accurate, but a nice approximation for debugging.
 */
function printView() {  
  $$('head link').select(function(l){ return l.getAttribute('media') == 'print'; }).invoke('setAttribute', 'media', 'all');
}

function detectBrowserFeatures() {
  Browser.features = (function() {
    // Modernizr code below, unmodified
      var tests = {
          canvas: true,
          rgba: true,
          hsla: true,
          multiplebgs: true,
          borderimage: true,
          borderradius: true,
          boxshadow: true,
          opacity: true,
          cssanimations: true,
          csscolumns: true,
          cssgradients: true,
          cssreflections: true,
          csstransforms: true,
          csstransitions: true
      };
      var passed = {};
      var groups = {
          canvas: ["canvas"],
          rgba: ["rgba"],
          hsla: ["hsla"],
          multiplebgs: ["multiplebgs"],
          borderimage: ["borderimage"],
          borderradius: ["borderradius"],
          boxshadow: ["boxshadow"],
          opacity: ["opacity"],
          cssanimations: ["cssanimations"],
          csscolumns: ["csscolumns"],
          cssgradients: ["cssgradients"],
          cssreflections: ["cssreflections"],
          csstransforms: ["csstransforms"],
          csstransitions: ["csstransitions"]
      };
      var enableHTML5 = true;
      var enableNoClasses = true;
      var m = document.createElement("modernizr");
      var detectFeature = function(feat) {
          var supported = false,
          prop,
          i,
          tmp;
          switch (feat) {
          case "canvas":
              supported = !!document.createElement("canvas").getContext;
              break;
          case "rgba":
              m.style.cssText = "background-color: rgba(150,255,150, .5)";
              supported = !!(m.style.backgroundColor.indexOf("rgba") !== -1);
              break;
          case "hsla":
              m.style.cssText = "background-color: hsla(120,40%,100%, .5)";
              supported = !!(m.style.backgroundColor.indexOf("rgba") !== -1);
              break;
          case "multiplebgs":
              m.style.cssText = "background: url(m.png), url(a.png), #f99 url(m.png);";
              tmp = m.style.background;
              i = 0;
              while (tmp.indexOf("url(") !== -1) {
                  i++;
                  tmp = tmp.substring(0, tmp.indexOf("url(")) + tmp.substring(tmp.indexOf("url(") + 4);
              }
              supported = !!(i === 3);
              tmp = i = null;
              break;
          case "borderimage":
              prop = ["borderImage", "webkitBorderImage", "MozBorderImage", "mozBorderImage", "oBorderImage", "msBorderImage"];
              m.style.cssText = "border-image: url(m.png) 1 1 stretch; -webkit-border-image: url(m.png) 1 1 stretch; -moz-border-image: url(m.png) 1 1 stretch; -o-border-image: url(m.png) 1 1 stretch; -ms-border-image: url(m.png) 1 1 stretch;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "borderradius":
              prop = ["borderTopRightRadius", "webkitBorderTopRightRadius", "MozBorderTopRightRadius", "mozBorderTopRightRadius", "oBorderTopRightRadius", "msBorderTopRightRadius"];
              m.style.cssText = "border-top-right-radius: 10px; -webkit-border-top-right-radius: 10px; -moz-border-top-right-radius: 10px; -o-border-top-right-radius: 10px; -ms-border-top-right-radius: 10px;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined && prop[i].indexOf("orderTopRight") !== -1) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "boxshadow":
              prop = ["boxShadow", "webkitBoxShadow", "MozBoxShadow", "mozBoxShadow", "oBoxShadow", "msBoxShadow"];
              m.style.cssText = "box-shadow: #000 1px 1px 3px; -webkit-box-shadow: #000 1px 1px 3px; -moz-box-shadow: #000 1px 1px 3px; -obox-shadow: #000 1px 1px 3px; -ms-box-shadow: #000 1px 1px 3px;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "opacity":
              m.style.cssText = "opacity: .5;";
              supported = !!(m.style.opacity.indexOf("0.5") !== -1);
              break;
          case "cssanimations":
              prop = ["animationName", "webkitAnimationName", "MozAnimationName", "mozAnimationName", "oAnimationName", "msAnimationName"];
              m.style.cssText = "animation: 'animate' 2s ease 2; -webkit-animation: 'animate' 2s ease 2; -moz-animation: 'animate' 2s ease 2; -o-animation: 'animate' 2s ease 2; -ms-animation: 'animate' 2s ease 2; position:relative;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "csscolumns":
              prop = ["columnCount", "webkitColumnCount", "MozColumnCount", "mozColumnCount", "oColumnCount", "msColumnCount"];
              m.style.cssText = "column-count: 3; -webkit-column-count: 3; -moz-column-count: 3; -o-column-count: 3; -ms-column-count: 3;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "cssgradients":
              m.style.cssText = "background-image: gradient(linear, left top, right bottom, from(#9f9), to(white)); background-image: -webkit-gradient(linear, left top, right bottom, from(#9f9), to(white)); background-image: -moz-gradient(linear, left top, right bottom, from(#9f9), to(white)); background-image: -o-gradient(linear, left top, right bottom, from(#9f9), to(white)); background-image: -ms-gradient(linear, left top, right bottom, from(#9f9), to(white));";
              supported = !!(m.style.backgroundImage.indexOf("gradient") !== -1);
              break;
          case "cssreflections":
              prop = ["boxReflect", "webkitBoxReflect", "MozBoxReflect", "mozBoxReflect", "oBoxReflect", "msBoxReflect"];
              m.style.cssText = "box-reflect: right 1px; -webkit-box-reflect: right 1px; -moz-box-reflect: right 1px; -o-box-reflect: right 1px; -ms-box-reflect: right 1px;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "csstransforms":
              prop = ["transformProperty", "webkitTransform", "MozTransform", "mozTransform", "oTransform", "msTransform"];
              m.style.cssText = "transform: rotate(3deg); -webkit-transform: rotate(3deg); -moz-transform: rotate(3deg); -o-transform: rotate(3deg); -ms-transform: rotate(3deg);";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          case "csstransitions":
              prop = ["transitionProperty", "webkitTransitionProperty", "MozTransitionProperty", "mozTransitionProperty", "oTransitionProperty", "msTransitionProperty"];
              m.style.cssText = "transition: all .5s linear; -webkit-transition: all .5s linear; -moz-transition: all .5s linear; -o-transition: all .5s linear; -ms-transition: all .5s linear;";
              for (i in prop) {
                  if (m.style[prop[i]] !== undefined) {
                      supported = true;
                      break;
                  }
              }
              break;
          default:
              supported = false;
          }
          m.style.cssText = "";
          tmp = i = prop = null;
          return supported;
      };
      function init() {
          if (enableHTML5 && !(!
          /*@cc_on!@*/
          0)) {
              var e = "abbr article aside audio bb canvas datagrid datalist details dialog figure footer header mark menu meter nav output progress section time video".split(" "),
              i = e.length;
              var n;
              while (i--) {
                  n = document.createElement(e[i]);
              }
              n = null;
          }
          for (var feat in tests) {
              if (tests[feat]) {
                  passed[feat] = detectFeature(feat);
              }
          }
          var passedgroup,
          len;
          var classes = [];
          for (var group in groups) {
              passedgroup = true;
              for (i = 0, len = groups[group].length; i < len; i++) {
                  feat = groups[group][i];
                  if (!passed[feat]) {
                      passedgroup = false;
                      if (!enableNoClasses) {
                          break;
                      }
                  }
              }
              if (passedgroup) {
                  classes.push(group);
              } else {
                  if (enableNoClasses) {
                      classes.push("no-" + group);
                  }
              }
          }
          passed._enableHTML5 = enableHTML5;
          passed._enableNoClasses = enableNoClasses;
          document.getElementsByTagName("body")[0].className += " " + classes.join(" ");
          classes = null;
          return passed;
      }
      return init();
  })();
}

var DataGridSorter = {
  initialize: function() {
     Dispatcher.register("click", function(event) { 
       var element = event.element();
       if (element.up('div.datagrid table.header th')) {    
         this.doSort(element);
       }
     }.bind(this));
  },
  _getSortFunction: function(element) {
    var className = element.classNames().detect(function(name) { return name.match(/^sort-(.+)$/); });
    if (className) {
      var match = className.match("sort-(.+)$");
      if (match) {
        return SPICEWORKS.utils.sortFunctions[match[1]];
      }
    }
  },
  sortRows: function(rows, index, direction, func) {
    var sorted = $A(rows).sortBy(function(element) { 
      return func(element.select('td')[index]);
    });
    
    if (direction == 'desc') { return sorted.reverse(); }
    else { return sorted; }
  },
  _clearMarkers: function(clicked) {
    clicked.removeClassName('sorted');
    clicked.removeClassName('sorted-asc');
    clicked.removeClassName('sorted-desc');
    
    var ths = clicked.siblings();
    ths.invoke("removeClassName", "sorted");
    ths.invoke("removeClassName", "sorted-asc");
    ths.invoke("removeClassName", "sorted-desc");
  },
  doSort: function(clicked, direction) {
    // Handle the case when there's other stuff in the TH
    if (clicked.tagName.toLowerCase() != "th") {
      clicked = clicked.up('th');
    }
    
    var func = this._getSortFunction(clicked);    
    if (!func) { return; }
    var sortIndex = clicked.up().select(clicked.tagName).indexOf(clicked);    
    var datagrid = clicked.up('div.datagrid');
    var table = datagrid.down('table.body');
    var rows = table.select('tr');
    var currentSort;

    if (clicked.hasClassName('sorted')) {
      currentSort = (clicked.hasClassName('sorted-asc') ? 'asc' : 'desc');
    }
    
    var newDirection = (currentSort == 'asc' ? 'desc' : 'asc');
    this._clearMarkers(clicked);
    
    var tbody = table.down("tbody");

    if (direction) {
      newDirection = direction;
    }

    this.sortRows(rows, sortIndex, newDirection, func).each(function(r) {
      tbody.insert(r);
    });
    
    clicked.addClassName('sorted');
    clicked.addClassName('sorted-' + newDirection);
    
    table.select('tr').invoke('removeClassName', 'every-other');
    table.select('tr:nth-child(odd)').invoke('addClassName', 'every-other');
  }
};
Event.register(DataGridSorter);

var RemoteControl = {
  connect: function(id){
    var path = '/asset/remote_control/'+ id +'?category='+encodeURI(Toolbar.options.baseCategory)+'&sourced_from='+Toolbar.options.sourcedFrom;
    if (this.seenRemoteControlInfo) {
      window.location.href = path;
    }
    else {
      this.seenRemoteControlInfo = true;
      new Ajax.Request(path);
    }
  }
};

var RemoteCollectorPoller = {
  initialize: function() {
    this.update(240, 120);
  },
  update: function(frequency, delay) {
    if ((!this.frequency) || (this.frequency != frequency)) {
      this.frequency = frequency;
      this.delay = delay;
      this.pollForStatus();
    }
  },
  stop: function() {
    if (this.poller) { this.poller.stop(); }
  },
  pollForStatus: function() {
    this.stop();
    this.poller = new Ajax.PeriodicalUpdater('', '/settings/network/check_remote_collector_status',  { frequency: this.frequency, delayedStart: (this.delay ? this.delay : false), parameters: {  requestClass: 'remote_collector/status' } }); 
  }
};

// Can't go in inventory.js because the help desk uses some functions, like kill process
var Asset = {
  reload:function(id) {
    new Ajax.Request('/asset/reload/' + id, {parameters: {}, method: 'post'});    
  },
  refreshProcessList:function(did) {
    var containerId = "process_list_#{did}_list_results".interpolate({did: did});
    var asc, th, index, container = $(containerId);
    th = container.down("th.sorted");    
    if (th) {
      asc = th.hasClassName('sorted-asc');
      index = th.up().select("th").indexOf(th);
    }
    LoadingMessage.set(container, "Refreshing…", {'class':'small', matchHeight:true});
    
    new Ajax.Request("/asset/process_list", {method:'get', parameters: {id: did, reload:true, initialized:true},
      onComplete:function(transport) {
        if (index >= 0 && $(containerId)) {
          DataGridSorter.doSort($(containerId).select("table.header th")[index], (asc ? "asc" : "desc"));
        }
      }
    });
  },
  killProcess:function(did, pid, info) {
    var row = $("process_list_#{deviceId}-#{pid}".interpolate({deviceId: did, pid: pid}));
    var status = new Element('td', {'class': 'confirmation status', 'colspan':6}).update(buildConfirmMessage(info));
    status.insert(new Element('a', {href: SUI.voidLink, 'class': 'really'}).update("End Process").observe("click", sendKillRequest));
    status.insert(new Element('a', {href: SUI.voidLink, 'class': 'cancel'}).update("Cancel").observe("click", cancel));
    
    displayConfirmation();
    
    function cancel() {
      row.select('td').invoke('show');
      status.remove();
    }
    
    function displayConfirmation() {
      row.select('td').invoke('hide');
      row.insert(status);
    }
    
    function sendKillRequest() { 
      // Dear sir or madam, I kindly request that you be killed. Regards.
      new Ajax.Request("/asset/kill_device_process.json", {method: 'post', parameters: {id: did, pid: pid, name: info['name']},
        onLoading: function() {
          status.update("Attempting to End Process…");
        },
        onComplete:function(transport) {
          var response = transport.responseText.evalJSON();
          status.removeClassName('confirmation');
          if (response['success']) {
            status.addClassName('success');
            status.update("Process ##{pid} Ended".interpolate({pid:pid}));
            var table = row.up('table.body');
            row.fade({duration:2, afterFinishInternal: function() {
              row.remove();
              table.select('tr').invoke('removeClassName', 'every-other');
              table.select('tr:nth-child(odd)').invoke('addClassName', 'every-other');
            }});
          }
          else {
            status.addClassName('failure');
            status.update("Could Not End Process ##{pid}: #{message}".interpolate({pid: pid, message:response['message']}));
            status.insert(new Element('a', {href: SUI.voidLink}).update("OK").observe("click", cancel));
          }
        }});
    }
    
    function buildConfirmMessage(info) {
      if (info && info['name'] && info['device_name']) {
        return "Are you sure you want to end <em>#{process}</em> on <em>#{device}</em>?".interpolate({process: info['name'], device: info['device_name']});
      }
      else {
        return "Are you sure you want to end this process?";
      }
    }
  },
  loadSummary:function(url, details){
    new Ajax.Request(url, {
      method:'get',
      onLoading:function(){ 
        this.summaryLoading(details);
      }.bind(this),
      onComplete:function(transport){
        if (200 == transport.status) {
          this.summaryLoaded(details);
        }
        else {
          this.summaryFailed(details);
        }  
      }.bind(this),
      parameters:{
        requestClass: 'secondary',
        tab: details['tab'] || Application.state.tab, 
        asynchronous:true,
        evalScripts:true}
      });
  },
  updateBreadcrumbs: function(details) {
    // {name: name, id: id, modeL: model}
    
    var header = $('secondary').down("div.sui-header h2");
    var name = details.name || "";
    
    if (!header) return;
    if (header.down("span.crumb.last")) {
      header.down("span.crumb.last").update(name);
    }
    else {
      var crumbSeparator = new Element("span", { className:"crumb-separator"}).update("›");
      var lastCrumb = new Element("span", {className:"crumb last"}).update(name);
      header.insert(crumbSeparator);
      header.insert(lastCrumb);
    }
  },
  summaryLoading:function(details){
    SPICEWORKS.fire("app:ui:secondary:unload");
    SPICEWORKS.fire("app:inventory:group:item:loading", {assetId: details['id'], assetType: details['type']});    
    LoadingMessage.set(Panels.activeContent(), "Loading…");
    this.updateBreadcrumbs(details);
  },
  summaryLoaded:function(){ 
    // Events are fired when the document is rendered
    Panels.hide();
    Panels.show("summary_wrap");
  },
  summaryFailed:function(details){
    SPICEWORKS.fire("app:inventory:group:item:failed", {assetId: details.id, assetType: details.type});
    StatusMessage.set(Panels.activeContent(), "Unable to load summary, please try again.", {'class': 'medium'});
  },
  save: function(url){    
    new Ajax.Request(url, {
      parameters:Form.serialize('item_edit_form'),
      method: 'post'
    });
    
    LoadingMessage.set("item_edit", "Saving &hellip;");
  },
  saveSoftware: function(){
    // without this anonymous wrapper, IE will not let the form submission continue on its merry way
    setTimeout(function(){
      LoadingMessage.set("item_edit", "Saving &hellip;");
    }, 100);
  },
  edit: function(id) {
    Panels.flip($("item_edit"), $("item_summary"));
  },
  deleteConfirmation: function() {
    Panels.flip($("item_delete"), $("item_summary"));
  },
  cancelEdit: function(formId){ 
    Panels.flip($("item_summary"), $("item_edit"));
    // remove this form now, which reopens #13699 but closes #13870
    // see Justin for background info
    Form.reset($("item_edit").down("form"));
    this.done(); 
  },
  cancelDelete:function() {
    Panels.flip($("item_summary"), $("item_delete"));
    this.done(); 
  },
  done: function(){
    SPICEWORKS.fire("app:inventory:group:item:edit:finish");
  },
  failed: function(){
    SPICEWORKS.fire("app:inventory:group:item:edit:failed");
  },
  flipToView: function() {
    Panels.flip(this.viewer, this.editor);
  },
  flipToEdit: function() {
    Panels.flip(this.editor, this.viewer);
  },
  toggleKeyEditor: function(){
    var wrapper = $('manage-software-keys');
    if (wrapper.hasClassName('open')){
      wrapper.removeClassName('open');
    } else {
      wrapper.addClassName('open');
      if (wrapper.hasClassName('loaded')){
      } else {
        LoadingMessage.set('computer-keys-list', 'Loading Software Keys &hellip;');
        new Ajax.Request('/asset/software/' + Toolbar.options.node, {method:'get'});
      }
    }
  },
  addSoftwareKey: function(control){
    var software = control.value, option = control.selectedIndex;
    control.selectedIndex = 0;
    var row = $('edit-software-' + software);
    if (row){
      var keyInput = row.down('input.text');
      if (row.hasClassName('has-key')){
        row.highlight();
      } else {
        row.addClassName('has-key');
        keyInput.disabled = false;
      }
      
      keyInput.focus();
    }
  },
  removeSoftwareKey: function(activator){
    activator = $(activator);
    var row = $(activator).up('tr'), input = row.down('td.product-key input');
    row.removeClassName('has-key');
    input.value = '';
  },
  softwareKeyIPEBuilt: function(ipe, form){
    if (ipe._controls.editor.value == 'click to edit') ipe._controls.editor.value = '';
  },
  softwareKeyIPEComplete: function(response, span){
    // once the value of a cell is updated, the row cache in the sortable table needs to be refreshed
    var table = span.up('table'), sortable = SortableTableManager.registered_sortables.detect(function(pair){ return pair.value.table == table; });
    if (sortable && sortable.value) sortable.value.cacheRows();
  }
};

var ChartManager = {
  charts:{},
  initialize: function() {
    Event.observe((document.onresize ? document : window), "resize", this.resizeToFit.bindAsEventListener(this));
  },
  newChart: function(chartContainer, options) {
    chartContainer = $(chartContainer);
    var initialization = "/fc/#{type}.swf?ChartNoDataText=#{noDataText}".interpolate({type: options['type'], noDataText: encodeURIComponent(options['noDataText'])});
    var name = '#{name}ChartId'.interpolate(options);
    var resize = (options['width'] == "auto");    
    var myChart = new FusionCharts( initialization, name, (resize ? this._widthOf(chartContainer) : options['width']), options['height'], 0, 0);
    if (options.dataURL) {
      myChart.setDataURL(options.dataURL);
    }
    else if (options.dataXML) {
      myChart.setDataXML(options.dataXML);
    }
    
    myChart.render(chartContainer);

    if (resize) {
      this.charts[chartContainer.id] = myChart;
    }
  },
  resizeToFit: function()  {
    var that = this;    
    clearTimeout(that.resizeTimer);
    that.resizeTimer = window.setTimeout(function() { 
      Object.keys(that.charts).each(function(key) {
        if ($(key)) {
          if (that._widthOf(key) != that.charts[key].getAttribute('width')) {
            that.charts[key].setAttribute('chartWidth', that._widthOf(key));
            that.charts[key].setAttribute('width', that._widthOf(key));
            that.charts[key].render(key);
          }
        }   
      });
    }, 300);
  },
  _widthOf: function(key) {
    var el = $(key).up();
    var width = el.getWidth();

    [el.getStyle('paddingRight'), el.getStyle('paddingLeft'), el.getStyle('marginLeft'), el.getStyle('marginRight')].each(function(e) {
     width = width - parseInt(e, 10);
    });

    return (width);
  }
};
Event.register(ChartManager);
