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

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

var Application = {
  runMode:'production',
  cobranded:false,
  uuid:null,
  authenticityToken:null,
  initialize: function(){
    if (console.needsSetup) console.initialize(); // sets up a pseudo-firebug style console window when firebug does not exist and we are in dev mode
  },
  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;
  },
  
  updateUriAnchorParams: function(params, options) {  
    var updateOptions = Object.extend({merge:false, 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(/\#+/, '#');
    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) {
    console.log( window.history.length);
    if(window.history.length == 1){
      document.location = url;
    }else{
      window.history.back();
    }
  },
  
  _scanStarted: function(options){
    document.fire('scan:started');
    this.options.scanRunning = true;
    // renew the poller, since when the scan is running the polling options are different
    this._startPolling(options);
  },
  _scanStopped: function(options){
    document.fire('scan:stopped');
    this.options.scanRunning = false;
    // 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 } } );
  },
  _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 || '1';
    if ($('adframe')) {
      var src = $('adframe').src.toString();
      if (!src.match(/jsr\=./)) {
        src = src + "&jsr=" + ctx;
      }else{
        src = src.gsub(/jsr\=./,'jsr=' + ctx);
      }
      $('adframe').src = src;
    }
  }
  
};

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

// for debugging firebug style, in browsers that don't have firebug
if (!console){
  var console = {
    needsSetup:true,
    initialize: function(){
      if (Application.runMode != 'development') return;
      $(document.body).insert('<textarea id="fake-firebug-console" readonly="readonly" style="display:none;width:100%;height:15em;position:fixed;bottom:0;left:0"></textarea>');
      console.logger = $('fake-firebug-console');
    },
    log: function(string){
      // only output the message if we're in dev mode
      if (Application.runMode != 'development' || !console.logger) return;
      console.logger.insert(string + (Prototype.Browser.IE ? "<br />" : "\n"));
      if (!console.logger.visible()) console.logger.show();
    }
  };
}

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

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));
    document.observe('prototip:group_menu:shown', this.menuShown.bindAsEventListener(this));
    document.observe('prototip:group_menu:hidden', this.menuHidden.bindAsEventListener(this));    
  },  
  menuShown: function(event) {
    var options = event.memo;
    
    //hide the other group menus
    var groupMenus = TipManager.tipsWithTag("group_menu");
    groupMenus = groupMenus.select(function(m) { return (m.target != options.target); }.bind(this));    
    groupMenus.invoke('hide');   
  },
  menuHidden: function() { },
  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();
    $('agg_wrap_scroll').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;
    $('agg_wrap_scroll').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 = $('agg_wrap_scroll').scrollHeight + 50; //take into account the top and bottom messages
     var scrollTop = $('agg_wrap_scroll').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('agg_wrap_scroll', {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)  $('agg_wrap_scroll').setStyle('height:277px;', {duration:0.5});
    else $('agg_wrap_scroll').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');
  },
  counterMouseover:function(icon){ if (Browser.ie6) $(icon).addClassName('hover'); },
  counterMouseout:function(icon){ if (Browser.ie6) $(icon).removeClassName('hover'); },
  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);
    }
  },
  createMenu:function(catID, catType, catName, options) {
    var activator = $("group_menu_" + catID);  
    if(!activator.prototip) {
      if (!options) options = {};      
      var list, link, items, content = "<ul>";
      
      if (catType == "locked") return;
      
      // Would have liked to use the new Element contstructor in prototype, but IE has trouble seeing those onclicks
      options.content = "<ul>";
      
      if (catType != "active_directory" && catType != "built_in") { 
        options.content +="<li><a href='#' onclick='ClusterView.editName(" + catID + "); return false;'>Change Name/Icon...</a></li>";
        options.content +="<li><a href='#' onclick='ClusterView.editRules(" + catID + "); return false;'>Edit Rules...</a></li>";         
      }
      
      options.content +="<li><a href='#' onclick='ClusterView.deleteConfirmation(" + catID + ", \"" + catName + "\"); return false;'>Delete This Group...</a></li>";
      options.content += "</ul>";

      TipManager.newTip(activator, { 
        element: activator,
        content: options.content, 
        style: 'bigicon:menu', 
        tags:['group_menu'], 
        container: activator.up('li'), 
        containerStyle: "position:absolute; left:10px; top:10px;", 
        hideOn: {element: activator.up('a'), event: 'mouseout'},
        showOn: 'mouseover'
      });  
    } 
  }
};

var Messaging = {
  PREFIX: 'application_messaging_',
  initialize: function(){
    if ( !this.initialized ){
      this.initialized = true;
      this.expiredMessages = $A();
      this.container = $( 'application_messaging' );
      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('/view/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' ); }
};


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

    Event.observe(document, '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 ));

    this._boundCustomLinkMouseOver = this.customLinkMouseOver.bindAsEventListener(this);
    this._boundCustomLinkMouseOut = this.customLinkMouseOut.bindAsEventListener(this);
    
    var that = this;
    if (Browser.ie6){
      $$('#navigation dl.my_stuff dd.custom').each(function(item){
        that._attachCustomLinkObservers(item); // we have to spoof the hover pseudo-class for IE6
      });
    }
  },
  shouldAllowAddNewClick: function(){ return !this.showingNewForm; },
  toggleDelete: function(){
    if (this.editMode) this.toggleEdit();
    if (!this.deleteMode){
      this.lists.get('navigation_my_stuff').list.addClassName('deleteable');
/*      Messaging.push('delete_link', "Click on the X icon next to the link to delete");*/
      this.deleteMode = true;
    } else {
      this.lists.get('navigation_my_stuff').list.removeClassName('deleteable');
/*      Messaging.pop('delete_link');*/
      this.deleteMode = false;
    }
  },
  toggleEdit: function(){
/*    console.log('toggleEdit');*/
    if (this.deleteMode) this.toggleDelete();
    if (!this.editMode){
/*      console.log('turnOn');*/
      this.lists.get('navigation_my_stuff').list.addClassName('editable').select('dd.editable').invoke('removeClassName', 'editing');
/*      Messaging.push('edit_link', "Click on the edit icon next to the link to edit");*/
      this.editMode = true;
    } else {
/*      console.log('turnOff');*/
      
      this.lists.get('navigation_my_stuff').list.select('form').invoke('remove');
      this.lists.get('navigation_my_stuff').list.removeClassName('editable');
/*      Messaging.pop('edit_link');*/
      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){
/*    console.log('cancelEdit');*/
    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){
    var dd;
    item = $(item);
    if (item) dd = item.up('dd');
    if (dd && Browser.ie6) this._attachCustomLinkObservers(dd); // we have to spoof the hover pseudo-class for IE6
  },
  newNavigationItemDestroyed: function(item){
    if (this.deleteMode) this.toggleDelete();
    var dd;
    item = $(item);
    if (item) dd = item.up('dd');
    if (dd && Browser.ie6) this._detachCustomLinkObservers(dd); // remove our mouseover/mouseout listeners for IE
  },
  customLinkMouseOver: function(event){
    var item = event.element();
    if (item.tagName.toString().toLowerCase() != 'dd') item = item.up('dd');
    item.addClassName('hover');
  },
  customLinkMouseOut: function(event){
    var item = event.element();
    if (item.tagName.toString().toLowerCase() != 'dd') item = item.up('dd');
    item.removeClassName('hover');
  },
  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();
    }
  },
  _attachCustomLinkObservers: function(link){
    link.observe('mouseover', this._boundCustomLinkMouseOver);
    link.observe('mouseout', this._boundCustomLinkMouseOut);
  },
  _detachCustomLinkObservers: function(link){
    link.stopObserving('mouseover', this._boundCustomLinkMouseOver);
    link.stopObserving('mouseout', this._boundCustomLinkMouseOut);
  },
  _initializeSponsorshipBox: function(){
    var url = this.sponsorshipURL;
    url += ( url.include('?') ? '&' : '?');
    url += 'cobranded=' + Application.cobranded + '&uuid=' + escape(Application.uuid);
    DynamicScriptInclude.load( url );
  }
};

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

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);
    document.observe('pivot:shown', this.events.adjustZIndexes);
    document.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);
    document.stopObserving('pivot:shown', this.events.adjustZIndexes);
    document.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) { tip.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);
    
    var iFrame = tipObject.wrapper.next('iframe');
    if (iFrame) {
      iFrame.setStyle("z-index:" + (options.zIndex - 1));
    }
  },
  _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
    document.observe(Event.TICKET.START_EDIT, this.events.refreshTips);
    document.observe(Event.TICKET.FINISH_EDIT, this.events.refreshTips);
    document.observe(Event.GENERAL_SUMMARY.START_EDIT, this.events.refreshTips);
    document.observe(Event.GENERAL_SUMMARY.FINISH_EDIT, this.events.refreshTips);
    document.observe("editable-table:start-edit", this.events.refreshTips);
    document.observe("editable-table:add-new", this.events.refreshTips);
    document.observe("editable-table:save-edit", this.events.refreshTips);
    document.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
    document.observe('flyover:shown', this.events.disable);
    document.observe('flyover:hidden', this.events.enable);
    document.observe('email_form:hidden', this.events.enable);
    document.observe('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
    document.stopObserving(Event.TICKET.START_EDIT, this.events.refreshTips);
    document.stopObserving(Event.TICKET.FINISH_EDIT, this.events.refreshTips);
    document.stopObserving(Event.GENERAL_SUMMARY.START_EDIT, this.events.refreshTips);
    document.stopObserving(Event.GENERAL_SUMMARY.FINISH_EDIT, this.events.refreshTips);
    document.stopObserving("editable-table:start-edit", this.events.refreshTips);
    document.stopObserving("editable-table:add-new", this.events.refreshTips);
    document.stopObserving("editable-table:save-edit", this.events.refreshTips);
    document.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);
    document.stopObserving('flyover:shown', this.events.disable);
    document.stopObserving('flyover:hidden', this.events.enable);
    document.stopObserving('email_form:hidden', this.events.enable);
    document.stopObserving('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 }, skipApplicationLocking: true } );
    }
  },
  _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);
    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) {
          console.error("Could not fix help tip: " + tipObject.options.traitID);
          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 = {
  initialize: function(options){
    this.options = Object.extend(this._defaultOptions, options || {});

    if (this.options.inventoryMode){
      this._attachObserverCollection(this._inventoryObservers);
      var that = this;
      $w('icon_switcher_browse bulk_operations_outer').each(function(item){
        that.elements.set(item, $(item));
      });
    }
    
    if (this.options.browseMode) this.options.sourcedFrom = 'browse';
    if (this.options.glideMode) this.options.sourcedFrom = 'glide';
    this._attachObserverCollection(this._baseObservers);
    
    document.observe('scan:started', this.scanStarted.bindAsEventListener(this));
    document.observe('scan:stopped', this.scanStopped.bindAsEventListener(this));

    this.flipActions();
  },

  scanStarted: function(){
    var scanLink = $('toggle_scan_link');
    if (scanLink){
      scanLink.removeClassName('not_scanning').addClassName('scanning').update('<span class="inner"><span class="icon">Stop Scan</span></span>');
    }
  },
  scanStopped: function(){
    var scanLink = $('toggle_scan_link');
    if (scanLink)
      scanLink.removeClassName('scanning').addClassName('not_scanning').update('<span class="inner"><span class="icon">Start Scan</span></span>');
  },
  update: function(options, urlOptions){
    urlOptions = Object.extend({updateUrl: true, merge: true}, urlOptions || {});
    
    var anchorParams;
    // ignore the sourcedFrom parameter if we're already in browse or glide mode, since we don't want to lose that state
    if ((options && this.options.browseMode) || (options && this.options.glideMode)) delete options.sourcedFrom;
    this.options = Object.extend(this.options, options || {});
    
    this.options.activeMode = null;
    this.flipActions();
    this.toggleButtonVisibility();
    this.updateSwitcherLinks();
    
    if (this.options.browseMode) {
      anchorParams = {path: this.options.path};
    }
    else if (this.options.glideMode) {
      anchorParams = {id: this.options.node, model: this.options.nodeType, group: this.options.baseCategory };
    }
    else {
      anchorParams = {id: this.options.node, model: this.options.nodeType };
    }
    
    if (urlOptions.updateUrl) {
      Application.updateUriAnchorParams(anchorParams, {merge: urlOptions.merge});      
    }
  },
  updateSwitcherLinks: function(options) {
    var new_url = '', nodeID = (this.options.nodeBaseType ? this.options.node : null);
    if ($('icon_switcher_view') && !this.options.iconMode){
        if ($A([null, '', 'all_devices']).include(this.options.baseCategory) && !this.options.node) {
          new_url = '/admin/from_toolbar';          
        }
        else if (this.options.baseCategory == "installables") {
          new_url = '/software/list/' + (this.options.nodeCategory ? this.options.nodeCategory : 'installables');
        }
        else {          
          new_url = "/view/" + this.options.baseCategory + "/devices";  
        }
    
      new_url += Application.buildUriAnchorQueryString($H({model: this.options.nodeType, id: nodeID}).compact());
      $('icon_switcher_view').href = new_url;
    }    
    
    if ($('icon_switcher_browse') && !this.options.browseMode){
      var new_url = '/inventory?';
      new_url += $H({model: this.options.nodeType, id: nodeID, category: this.options.baseCategory}).compact().toQueryString();
      $('icon_switcher_browse').href = new_url;
    }
    
    if ($('icon_switcher_glide') && !this.options.glideMode){
      var new_url = '/view/glide';
      new_url += Application.buildUriAnchorQueryString($H({model: this.options.nodeType, id: nodeID, group: this.options.baseCategory}).compact());
      $('icon_switcher_glide').href = new_url;
    }
  },
  createTicket: function(event){
    event.stop();
    var postBody = 'from=' + this.options.sourcedFrom;
    var active_tab = IconView.getActiveTab();
    if (this.options.sourcedFrom && this.options.sourcedFrom.indexOf('ticket') < 0 && this.options.node) postBody += '&ticket[ticketable_id]='+this.options.node;
    if (this.options.sourcedFrom && this.options.sourcedFrom.indexOf('ticket') < 0 && this.options.node && this.options.nodeType) postBody += '&ticket[ticketable_type]='+this.options.nodeType;
    if (this.options.baseCategory) postBody += '&category='+ this.options.baseCategory;
    if (active_tab) postBody += '&active_tab=' + active_tab;
    new Ajax.Request('/tickets/new', { parameters:postBody });
  },
  createAsset: function(event){
    event.stop();
    if (this.options.canAddNewAsset){
      var request_path = '/asset/new?';
      if (this.options.sourcedFrom) request_path += 'from='+this.options.sourcedFrom;
      if (this.options.baseCategory) request_path += '&category_from=' + encodeURI(this.options.baseCategory);
      new Ajax.Request(request_path);
    }
  },
  editCurrentGroup: function() {
    var new_url = '/settings/categories?edit_category=' + this.options.categoryID;
    location.href = new_url;
  },
  reclassifyActionMain: function(event){ this.reclassifyAction('reclassify', event); },
  reclassifyActionDelete: function(event){ this.reclassifyAction('delete', event); },
  reclassifyActionEdit: function(event) { this.reclassifyAction('edit', event); },
  groupCopy: function(event){
    event.stop();
    if (this.canCopy()){
      this.options.mode = 'copy';
      this.renderActions(this.options.node);
    }
    else if ((this.reasonCopyIsDisabled() == "createAnotherGroup") || (this.reasonCopyIsDisabled() == "createGroup")){
      // user has deleted all of their groups, or all but one of their groups      
      var postBody;
      postBody += '&mode=copy&category='+encodeURI(this.options.baseCategory)+'&from='+this.options.sourcedFrom+"&staticCount="+this.options.staticCount;      
      new Ajax.Request('/asset/prompt_before_action', { parameters:postBody });
    }
  },
  reclassifyAction: function(mode, event){
    event.stop();
    this.options.mode = mode;
    if (this.options.canPerformActions) this.renderActions(this.options.node);
  },
  renderActions: function(selected_id){
    var postBody;
    if (selected_id) postBody = 'selected_id='+selected_id;
    else postBody = 'no_devices=true';
  
    postBody += '&mode='+this.options.mode+'&category='+encodeURI(this.options.baseCategory)+'&from='+this.options.sourcedFrom;

    new Ajax.Request('/asset/actions', { parameters:postBody });
  },
  renderComparison: function(left_id, right_id, category){
    var postBody;

    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;
    if (this.mode == 'compare') postBody += '&already_comparing=true';

    new Ajax.Request('/asset/compare', { parameters:postBody});
    this.mode = 'compare';
  },
  remoteControl: function(event){
    if (event) event.stop();
      var path = '/asset/remote_control?id='+this.options.node+'&category='+encodeURI(this.options.baseCategory)+'&sourced_from='+this.options.sourcedFrom;
      if (this.seenRemoteControlInfo) window.location.href = path;
      else {
        this.seenRemoteControlInfo = true;
        new Ajax.Request(path);
      }
  },
  toggleButtonVisibility: function(){
    var asset = this.elements.get('new_asset');
    if (asset) asset[this.options.canAddNewAsset ? 'removeClassName' : 'addClassName']('disabled');

    this.flipActions();
  },
  reasonMoveIsDisabled: function() {
    if (!this.options.inSmartGroup && this.options.baseCategory != 'installables' && this.options.deviceCount > 0 && this.options.groupCount == 1) {
      return "createGroup";
    }    
    else {
      return false;
    }
  },
  reasonCopyIsDisabled: function() {
    if (this.options.deviceCount > 0 && this.options.baseCategory != 'installables' && this.options.groupCount <= 1) {
      return "createAnotherGroup";
    }
    else if (this.options.inSmartGroup && this.options.baseCategory != 'installables' && this.options.deviceCount > 0 && this.options.groupCount == 0) {
      return "createGroup";
    }
    else {
      return false;
    }
  },
  canMove: function() {
    return (this.options.deviceCount > 0) && (this.options.groupCount > 1) && this.options.baseCategory != 'installables';
  },
  canCopy: function() {
    return (this.options.deviceCount > 0) && (this.options.groupCount > 1) && this.options.baseCategory != 'installables';
  },  
  flipActions: function(){
    var that = this;
    $w('reclassify_action reclassify_action_delete').each(function(item){
      var inner_item = that.elements.get(item);
      if (inner_item) inner_item[that.options.canPerformActions ? 'removeClassName' : 'addClassName']('disabled');
    });
    
    if ($('asset_form_button')) {
      $('asset_form_button')[that.options.canAddNewAsset ? 'removeClassName' : 'addClassName']('disabled');
    }
    
    var move = this.elements.get('group_move');
    if (move) { 
      var willMove = (this.canMove() || this.reasonMoveIsDisabled());
      move[willMove ? 'removeClassName' : 'addClassName']('disabled');
    }
    
    var copy = this.elements.get('group_copy');
    if (copy) { 
      var willCopy = (this.canCopy() || this.reasonCopyIsDisabled());
      copy[willCopy? 'removeClassName' : 'addClassName']('disabled');
    }
  },
  
  generatePopup: function(id, title, content){ new Popup(id, title, '<p>' + content + '</p>', {closeable:true}); },
  _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,
    groupCount: null,
    activeGroupType: null,
    activeMode:null, // null is steady-state, can also be reclassify, compare, etc. which are transient states
    inventoryMode:true,
    iconMode:false,
    browseMode:false,
    glideMode:false,
    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_edit_action':           'reclassifyActionEdit',
    'reclassify_action':          'reclassifyActionMain',
    'group_move':                 'groupMove',
    'group_copy':                 'groupCopy',
    'reclassify_action_delete':   'reclassifyActionDelete',
    'edit_current_group':         'editCurrentGroup'
  },
  _baseObservers:{
    'asset_form_button':          'createAsset',
    'helper_ticket_form_button':  'createTicket'
  }
};

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

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 QuickFilter = {
  filter: function(element) {

    element = $(element);
    
    // there is a span that is a child of the span we're looking for, so if we got the child one, let's get the parent
    if (!element.hasClassName('count')) element = element.parentNode;

    var quickfind;
    if (quickfind = element.getAttribute('quickfind')){
      if (element.hasClassName('software')) {
        this.triggerSearch('@' + quickfind + '@');
      }
      else {
        LiveSearch.filter('viewer', 'quickfind', '@' + quickfind + '@'); 
      }      
    }
  },
  triggerSearch: function(term) {
    if ( !term ) term = '';
    this.searchBox = $('quickfind');
    this.searchForm = this.searchBox.up('form');
    this.searchBox.value = term;
    this.searchForm.onsubmit();
  }
};

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

    var deviceDetail = $('device_detail');
    deviceDetail.observe('click', this.deviceDetailClicked.bindAsEventListener(this));
    if (Browser.ie6){
      deviceDetail.observe('mouseover', this.deviceDetailMouseover.bindAsEventListener(this));
      deviceDetail.observe('mouseout', this.deviceDetailMouseout.bindAsEventListener(this));
    }

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


  },

  registerIcon:function(link, showInfoBox, deviceID){
    link = $(link);
    if (deviceID && Toolbar.options.node && Toolbar.options.node == deviceID) $('device_' + deviceID).addClassName('selected');
    if (this.listeners) {
      link.observe('click', this.listeners.iconClick);
      if (showInfoBox) link.observe('mouseover', this.listeners.iconMouseover);      
    }
  },
  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'));
  },
  iconMouseover: function(event){
    var element = event.findElement('li');
    AssetPopupManager.loadForAsset(element.getAttribute('item_id'));
  },
  iconClicked:function(e){
    var clickedLink = e.findElement('a');
    var clickedItem = clickedLink.up('li.icon');

    e.stop();
    
    if (!clickedItem.hasClassName('unclickable'))
      this.drawSummary(clickedItem.getAttribute('click_url'), clickedItem);
  },
  drawSummary:function(url, itemElement){
    this.summaryLoading(itemElement);
    new Ajax.Request(url, { method: 'get', 
      parameters:{
        tab: TabbedBox.activeTab, 
        section: TabbedBox.activeSection}, 
        asynchronous:true, 
        evalScripts:true,
        onFailure:function(request){
          this.summaryFailed();
          this.summaryLoaded();
        }
    });
  },
  showDeviceSummary:function(){
    var deviceSummary = $('device_summary');
    if (deviceSummary && !deviceSummary.visible()) new Effect.BlindDown('device_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;
  },
  deviceDetailClicked:function(e){
    if (e.element().hasClassName('count') || e.element().up().hasClassName('count')) return;
    if (LiveSearch.active) {
      LiveSearch.clear('viewer', "Filter " + this.categoryName);
    }
    else {
      $$('#viewer ul li.selected').invoke('removeClassName', 'selected');
      $$('#software_list_data tr.clicked').invoke('removeClassName', 'clicked');
      
      Application.updateUriAnchorParams({}, {merge: false});      
      if (this.summaryRequest) { this.summaryRequest.transport.abort(); }
      this.summaryRequest = new Ajax.Request('/inventory/reload_group_summary' , { parameters: {group: + Toolbar.options.categoryID}, skipApplicationLocking: true });
    }
  },
  summaryLoading:function(selected){
    selected = $(selected);

    $$('#viewer ul li.selected').invoke('removeClassName', 'selected');
    selected.addClassName('selected');
  },
  summaryLoaded:function(){  },
  summaryFailed:function(){
    var summary = $('summary_wrap');
    summary.update('<div id="device_summary" class="no_items"><h3 class="heading"></h3><div id="tab_box" class="no_tabs"><div id="active_overview"><h4 class="error">Unable to load summary, please try again.</h4></div></div></div>');
  },
  scrollSelectedIntoView:function() {
    $$$('#viewer ul li.selected a').focus();
  },
  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 = $$('#viewer 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'));
          anchorParams.set('model', firstItem.getAttribute('item_model'));
        }
      }
    } else {
      fetchItem = true;
    }
    
    if (!fetchItem) 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 + "?" + 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') });
    
    new Ajax.Request(baseUri, {
      onComplete:function(){
        this.summaryLoaded();
      }.bind(this), 
      onFailure:function(){this.summaryFailed();},
      onLoading:function(){ 
        this.summaryLoading('device_' + anchorParams.get('id'));
        this.scrollSelectedIntoView();
      }.bind(this)});
    var selected = $('device_' + anchorParams.get('id'));
    if (selected) selected.addClassName('selected');
  },
  deviceDetailMouseover:function(e){
    var element = e.findElement( 'div');
    element.addClassName('hover');
  },
  deviceDetailMouseout:function(e){
    var element = e.findElement( 'div');
    element.removeClassName('hover');
  },
  getActiveTab:function() {
    var summary_tabs = $('summary_tabs');
    var active_tab;
    if (summary_tabs) active_tab = summary_tabs.down('.active').id; 
    return active_tab;
  },
  flipCommentView:function(show_attachment){
    if (show_attachment){
      $('add_comment').hide();
      $('add_attachment').show();
      if ($('comment_is_public')) $('attachment_comment_is_public').checked = $('comment_is_public').checked;
      $('attachment_comment_body').value = $('comment_body').value;
    } else {
      $('add_comment').show();
      $('add_attachment').hide();
      if ($('comment_is_public')) $('comment_is_public').checked = $('attachment_comment_is_public').checked;
      $('comment_body').value = $('attachment_comment_body').value;
      $('comment_attachment').value = '';
    }
  },
  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();
    }
  },
  alertCleared: function(){ this._incrementCounter(-1, 'alert'); },
  errorCleared: function(){ this._incrementCounter(-1, 'error'); },
  ticketRemoved: function(){ this._incrementCounter(-1, 'ticket'); },
  ticketAdded: function(){ this._incrementCounter(1, 'ticket'); },
  _incrementCounter: function(amountToIncrement, counterName){
    if (!this.categoryName) return; // this code should only run if we're looking at a category in icon view mode
    if (!amountToIncrement) amountToIncrement = 1;
    // probably shouldn't blindly increment/decrement the count of tickets on a category, in the event that the ticket created is not related to the current category
    var existingCount = $$('#device_detail span.count_wrap span.count_' + counterName);
    if (existingCount && existingCount.size() > 0){
      existingCount = existingCount.first().down('em');
      var newCount = parseInt( existingCount.innerHTML, 10 ) + amountToIncrement;
      var countWrapper = existingCount.up( 'span.count' );
      existingCount.update(newCount < 0 ? 0 : newCount);
      if (countWrapper.visible() && newCount < 1) countWrapper.hide();
      else if (!countWrapper.visible() && newCount > 0 ) countWrapper.show();
    }
  }
};

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 TabbedBox = {
  setActive:function(activeID, activeSection){
    this.activeID = activeID;
    this.activeSection = activeSection;
    this.activeTab = this.activeID.replace(/_tab/, '');

    $$('#summary_tabs li.active').invoke('removeClassName', 'active');
    Element.addClassName(activeID, 'active');
    
    var hashParams = $H();
    if (this.activeTab) hashParams.set('tab', this.activeTab);
    // active section is only valid if it's different from active tab
    if (this.activeSection) hashParams.set('section', this.activeSection);
    Application.updateUriAnchorParams(hashParams, {merge: true});
  }
};

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( '/view/rescan_similar_devices/?device_id=' + device + '&account=' + account + '&user_type=' + userType);
  }
};

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');
        LiveSearch.clearActiveState();
      }
    }
    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 LiveSearch = {
  keyInterval:null,
  search:function(list, query, label){
    if (query == ''){
      LiveSearch.clear(list, label);
    } else {
      var allItems = document.getElementById(list).getElementsByTagName('li');
      var reg = new RegExp(query, "i"), selectedNode;
      var currentItem, search, searchable;

      for (var i=0;i<allItems.length;i++) {
        if (allItems[i].className.indexOf('icon') > -1 && (currentItem = allItems[i])){
          if (currentItem.className.indexOf('selected') > -1) selectedNode = currentItem;
          currentItem = $(allItems[i]);
          searchable = currentItem.down('.search_text');
          if (searchable){
            search = Helpers.innerText(searchable);

            //To keep IE from crashing when there are no items that satisfy
            //the quicksearch, we put an extra li in that never gets deleted.
            if (currentItem.id != 'node_hideme') reg.test(search) ? currentItem.style.display='' : currentItem.style.display='none';
          } else currentItem.style.display='none';
        }
      }      
      $('clear_filtered_view').show();
      this.active = true;      
      if ($('device_detail')) { $('device_detail').writeAttribute('title', "Clear filtered view"); }
    }
  },
  clearActiveState:function(){
    var summaryWrap = $('summary_wrap');
    $$( 'div.detail_box' ).invoke( 'hide' );
    if (summaryWrap.visible()){
      // clear the selected icon, aka item with class == selected
      $('viewer').select('.selected').each(function(element){
        this.selectedItem = element;
        element.removeClassName('selected');
      }.bind(this));
    }
  },
  restoreActiveState:function() {
    var summaryWrap = $('summary_wrap');
    // $$( 'div.detail_box' ).invoke( 'show' );
    if (!summaryWrap.visible()){
      if (this.selectedItem) this.selectedItem.addClassName('selected');
    }
  },
  keypress:function(list, query, label){
    if (IconView.mode == 'software') LiveSearchTable.search(list, $F(query));
    else LiveSearch.search(list, $F(query), label);
  },
  checkForEnter: function( event, label ){
    if ( event.keyCode == Event.KEY_RETURN ) LiveSearch.keypress('viewer', 'quickfind', label);
  },
  clear:function(list, label){
    $A($(list).select('li')).each(function(item){
      item.show();
    });

    var input = $('quickfind');
    input.addClassName('init');
    input.value = label;
    $('clear_filtered_view').hide();
    this.active = false;
    if ($('device_detail')) { $('device_detail').writeAttribute('title', "Show group summary"); }
    this.restoreActiveState();
  },
  filter:function(list, input, query, category){
    if ($(list)){
      $(input).value = query;

      if (query == '') LiveSearch.clear(list, 'Search ' + category);
      else LiveSearch.search(list, query);
    }
  }
};

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();
      }
      document.fire(Event.NOTES.START_EDIT);
    }
  },
  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 } });

    document.fire(Event.NOTES.FINISH_EDIT);
  },
  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');
    
    document.fire(Event.NOTES.FINISH_EDIT);
  }
};

var GeneralSummary = {
  edit: function(from_group_edit){
    $('my_general_summary').hide();
    $('my_general_summary_form').show();
        
    document.fire(Event.GENERAL_SUMMARY.START_EDIT);
  }, 
  save: function(url){
    new Ajax.Request(url, {
      parameters:Form.serialize('general_summary_form')
    });
    GeneralSummary.done();
  },
  cancel: function(){ GeneralSummary.done(); },
  done: function(){
    $('my_general_summary').show();
    $('my_general_summary_form').hide();
    document.fire(Event.GENERAL_SUMMARY.FINISH_EDIT);
  }
};

var GroupMembershipEditor = {  
  initialize: function() {
    this.membershipContainer = $$$("form#general_summary_form div.group_membership");
    this.excludedList = this.membershipContainer.down('ul.excluded');
    this.manuallyAddedList = this.membershipContainer.down('ul.manually_added');
    this.autoAddedList = this.membershipContainer.down('ul.auto_added');      
    this.groupAdder = this.membershipContainer.down('#add_group');
    this.hiddenFields = this.membershipContainer.down('#group_edit_fields');
    
    this.groupState = {auto: $A(), manual: $A(),excluded: $A()};
    this.groupChanges = {exclude: $A(), include: $A(), tag: $A(), untag: $A() };
    
    this.addObservers();
    this.groupAdder.observe('change', this.manuallyAdded.bindAsEventListener(this));

    this._recordInitialState();
  },
  _recordInitialState: function() {
    var that = this;    
    this.groupState['auto'] = this.autoAddedList.select('li.group').collect(function(i) { return i.readAttribute('group_id'); } );;
    this.groupState['manual'] = this.manuallyAddedList.select('li.group').collect(function(i) { return i.readAttribute('group_id'); } );;
    this.groupState['excluded'] = this.excludedList.select('li.group').collect(function(i) { return i.readAttribute('group_id'); } );;
  },
  performAction: function(event) {
    // called when any group bubble is closed
    var element = event.element();
    var li = $(element).up('li.group');
    var id = li.getAttribute('group_id');
    
    if (li.up('ul.auto_added')) { this.addExclusion(id, li); }
    else if (li.up('ul.manually_added')) { this.removeUserTag(id, li); }
    else if (li.up('ul.excluded')) { this.removeExclusion(id, li); }
  },
  manuallyAdded: function(event) {
    // called on select box change
    var index = this.groupAdder.selectedIndex;
    if (index == 0) { return; }
    var name = this.groupAdder.options[index].innerHTML;
    var id = $F(this.groupAdder);
    var li = this._createGroupObject(id, name);
    
    this.groupAdder.selectedIndex=0;
    
    this.addUserTag(id, li);
    this._refresh();
  },
  addExclusion: function(id, li) {
    li.fade({duration:0.5, afterFinishInternal:function(){
      this.excludedList.insert(li, {position:content});
      this.excludedList.up('fieldset').show();
      li.appear({duration:0.5});
      li.down('span').writeAttribute('title', 'Stop excluding this device from this group');
      this._refresh();
    }.bind(this)});
    
    if (!this.groupChanges['exclude'].include(id)) {
      this.groupChanges['exclude'].push(id);
    }
    this._refresh();
  },
  removeExclusion: function(id, li) {
    li.fade({duration:0.5, afterFinishInternal:function(){
      if (this.groupState['auto'].include(id)) {
        this.autoAddedList.up('fieldset').show();
        this.autoAddedList.insert(li, {position:content});
        li.appear({duration:0.5});
        li.down('span').writeAttribute('title', 'Exclude this device fom this group');
      }
      this.groupChanges['exclude'] = this.groupChanges['exclude'].without(id);
    }.bind(this)});
    
    if (!this.groupState['auto'].include(id)) {
      this.groupChanges['include'].push(id);
    }
    this.groupChanges['exclude'] = this.groupChanges['exclude'].without(id);    
    this._refresh();
  },
  removeUserTag: function(id, li) {
    li.fade({duration:0.5, afterFinishInternal:function(){
      li.remove();
    }.bind(this)});
    
    if (this.groupChanges['tag'].include(id)) {
      this.groupChanges['tag'] = this.groupChanges['tag'].without(id);
    }
    else {
      this.groupChanges['untag'].push(id);
    }
    this._refresh();
  },
  addUserTag: function(id, li) {
    var exists = this.membershipContainer.select('ul.auto_added li.group.' + id + ', ul.manually_added li.group.' + id);
    if (exists.size() > 0) {
      exists.invoke('highlight');
    }
    else if (this.groupChanges['exclude'].include(id) && this.groupState['auto'].include(id)) {
      li = this.excludedList.down('li.' + id);
      this.removeExclusion(id, li);
    }
    else {
      if (!this.groupChanges['tag'].include(id)) {
        this.groupChanges['tag'].push(id);
        this.manuallyAddedList.up('fieldset').show();
        this.manuallyAddedList.insert(li, {position:content});
        li.highlight();
      }
    }
    this._refresh();
  },
  _refresh: function() {
    this.removeObservers();
    this.addObservers();
    this._updateHiddenFields();
  },
  _updateHiddenFields: function() {
    var that = this;
    this.hiddens = $A();
    $H(this.groupChanges).each(function(pair) {
      pair.value.each(function(value) {
        that.hiddens.push(new Element('input', {type:'hidden', value: value, name:"group_" + pair.key + "[]"}));
      });
    });
    this.hiddenFields.update('');
    this.hiddens.each(function(hidden) {
      that.hiddenFields.appendChild(hidden);
    });
  },
  _getCategoryID: function(id) {
    var match = id.match(/device_(\d+)/);
    return (!match ? null : match[1]);
  },
  removeObservers: function() {
    this.membershipContainer.getElementsBySelector('li.group').invoke('stopObserving', 'click');
  },
  _createGroupObject: function(id, name) {
      return (new Element('li', {'class': 'with_close group ' + id, 'group_id': id }).update(name + "<span class='closebutton' title ='Remove this device from this group'>remove</span>"));
  },
  addObservers: function() {
    var that = this;
    this.membershipContainer.getElementsBySelector('li.group').invoke('observe', 'click', that.performAction.bindAsEventListener(that));
  }
};

/* 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();
    }
  },
  showAllTickets: function() {
    this.hiddenTickets().each( function(item){
      item.show().removeAttribute('hidden');
    });
    $("ticket_count").up("tr").hide();
  },
  allTickets: function(){
    return $$("table#status_table tr.ticket");
  },
  hiddenTickets: function() {
    return $$("table#status_table tr.ticket").select(function(t){ 
      return (!t.visible() && t.getAttribute("hidden")=="true");
    });
  },
  _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 = $$( '#viewer 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 = {
  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();
    }
  },
  dismissAlert: function(alertID, alertCount){
    alertCount = parseInt(alertCount || '0', 10);
    IconView.alertCleared();
    $(alertID).hide();

    if ($('tickets_and_alerts_wrapper')) {   
      if ($('alert_count').visible) $('alert_count').update("2 of " + alertCount);
      if (alertCount == 0) {
        $('status_table').select('tr.alert, tr.alerts-header').invoke('hide');
        if (TicketManager.allTickets().size() == 0) $('tickets_and_alerts').hide();
      }
      var firstHidden = this._hiddenAlerts().first();
      if (firstHidden) firstHidden.show().removeAttribute('hidden');

      if (this._hiddenAlerts().size() == 0) $('status_table').down('tr.alert.action').hide(); //hide count and "show all" button
    }    
  },
  showAllAlerts: function() {
    this._hiddenAlerts().each( function(item) { item.show().removeAttribute('hidden'); });
    $("alert_count").up("tr").hide();
  },
  _hiddenAlerts: function() {
    return $$("table#status_table tr.alert").select( function(t) { 
      return (!t.visible() && t.getAttribute("hidden")=="true");
    });
  },
  _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 = $$( '#viewer 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.node, category: Toolbar.nodeCategory } } );
  }
};

var SimpleProgress = Class.create({
  /* options 
  hidePercent: doesn't show percent at all
  ignoreNegatives: prevents progress bar from shrinking
  showDecimals: shows decimal percent readings
  fixedPercentPosition: keeps percent reading on the right side of the progress bar, rather than floating along with the progress
  */

  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.hide();
    this._getDimensions();
  },
  _getDimensions: function() {
    this.containerDimensions = this.container.getDimensions();
    this.containerOffset = this.container.cumulativeOffset();
    this.containerRight = (this.containerOffset['left'] + this.containerDimensions['width']);
    this.percentageDimensions = this.percent.getDimensions();
    this.percentageOffset = this.percent.cumulativeOffset();
  },
  update: function(percentComplete) {
    /* don't show the progress bar decreasing */
    if ((percentComplete < this.lastPercent) && (this.options.ignoreNegatives)) return;
    else if (percentComplete < 0) percentComplete = 0;

    /* the container width is never 0, so if we think it is, we're wrong.. */
    if (this.containerDimensions['width'] == 0) this._getDimensions();

    percentComplete = (this.options.showDecimals ? Number(percentComplete) : Number(percentComplete).round());
    percentComplete = (percentComplete > 100) ? 100 : percentComplete;
    this.lastPercent = percentComplete;

    this.percent.update(percentComplete + " %"); 

    var projectedRight = Number(
    this.containerOffset['left'] + 
    (this.containerDimensions['width'] * Number(percentComplete/100)) + 
    this.percent.getWidth() + 
    (Number(this.percentage.getStyle('padding-left').replace('px','')) + Number(this.percentage.getStyle('padding-right').replace('px','')))
    ).ceil();

    var barRight = ((this.containerDimensions['width']) * (percentComplete/100)); 

    if (this.options.fixedPercentPosition) {
      /* if fixed position, just place percent at the end of the progress bar */
      this.percentage.absolutize(); 
      this.percentage.setStyle({ width: '100%'}); //we're right aligning, so make the field as big as possible to avoid anomalies when adding digits
      this.percentage.setStyle({ textAlign: 'right', left: (this.containerDimensions['width'] - this.percent.getWidth()) + 'px'});

      /*only show it after it has been initiallly positioned */
      if (!this.options.hidePercent) this.percentage.show();
    } else if ((projectedRight > this.containerRight) && (percentComplete > 50)) {
      /* if percentage is going to fall outside of the progress bar,  place at the end of the bar */
      this.percentage.absolutize();
      if (!this.options.hidePercent) { this.percentage.show(); }

      var leftPos=this.containerDimensions['width'] - this.percent.getWidth() - 
      Number(this.percentage.getStyle('padding-left').replace('px', '')) -
      Number(this.percentage.getStyle('padding-right').replace('px', '')) + 'px';

      new Effect.Morph(this.percentage, {style:{left:(leftPos)}});
    } else {
      /* this works better than letting it float naturally (for handling cases when the progress shrinks, for instance) */
      if (!this.options.hidePercent) { this.percentage.show(); } 
      this.percentage.absolutize(); 

      if (this.percentage.getStyle('left').startsWith('-')) { 
        /* sometimes the percentage will get absolutized out of view, and get stuck over there. In that case, 
        reset the style so it floats naturally to where it's supposed to be, and then absolutize it again. */
        this.percentage.setStyle('left:' + barRight + 'px');
      } else {
        new Effect.Morph(this.percentage, {style:{left: (barRight) + 'px' }});
      }
    }

    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');
    
    if (this.form && this.element.id != 'ticket_form') window.setTimeout(this.form.focusFirstElement.bind(this.form), 750);
    var draggableOptions = { handle: 'title', zIndex: 500 };
    // if (Browser.ie6) {
      this._fixIEOverlapping();
      Object.extend(draggableOptions, { 
        change: this._fixIEOverlapping.bind(this)
      });
    // }
    new Draggable(this.element, draggableOptions);
    
    QuickForm.forms.set(this.element.id, this);
    
    document.fire(Event.POPUP.OPEN);

    // we want to hide any active pivot menus when displaying a popup
    PivotManager.clearActive();
    SpiceSelectManager.clearActive();
  },
  
  // In IE we have to shimmy an iframe underneath the help window so that form
  // controls and/or Flash ads won't cover it up
  _fixIEOverlapping: function() {
    var id = this.element.id + '_iefix', iefix = $(id);
    if (!iefix) {
      iefix = new Element('iframe', {id:id, style:'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);', src:'javascript:void(function(){})', frameborder:0, scrolling:'no'}); 
      $(this.element).insert({after: iefix});      
      setTimeout(this._fixIEOverlapping.bind(this), 50);
      return;
    }
    iefix.clonePosition(this.element, { setTop: (!this.element.style.height) });
    iefix.style.zIndex = 499;
    this.element.style.zIndex = 500;
  },

  destroy: function(){
    this.element.fade({duration:0.5});
    
    document.fire(Event.POPUP.OPEN);
    
    var that = this;
    setTimeout(function(){
      that.element.remove();
      var iefix = $(that.element.id + '_iefix');
      if (iefix) iefix.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.5});
      }

      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' };
      if ((Browser.ie6) || (Browser.ie7)) {
        this._fixIEOverlapping();
        Object.extend(draggableOptions, { 
          change: this._fixIEOverlapping.bind(this)
        });
      }

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

      document.fire(Event.POPUP.OPEN);

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

  // In IE we have to shimmy an iframe underneath the help window so that form
  // controls and/or Flash ads won't cover it up
  _fixIEOverlapping: function() {
    var id = this.element.id + '_iefix', iefix = $(id);
    if (!iefix) {
      iefix = new Element('iframe', { id:id,src:'javascript:void(function(){})',frameborder:0,scrolling:0,style:'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);'});
      this.element.insert({after : iefix});
      setTimeout(this._fixIEOverlapping.bind(this), 50);
      return;
    }
    iefix.clonePosition(this.element, { setTop: (!this.element.style.height) });
    iefix.style.zIndex = 499;
    this.element.style.zIndex = 500;
  },

  /* 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();
      var iefix = $(this.element.id + '_iefix');
      if (iefix) iefix.remove();
    }else{
      new Effect.Fade(this.element,{duration:0.5});

      var that = this;
      setTimeout(function() {
        that.element.remove();
        var iefix = $(that.element.id + '_iefix');
        if (iefix) iefix.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) document.fire(Event.POPUP.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 );

    this.popup.stopObserving( 'mouseover', this.listeners.mouseOverPopup );
    this.popup.stopObserving( 'mouseout', this.listeners.mouseOutPopup );
    
    if (this.popup) 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 - $('device_list').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(){
    // fetch the popup through Ajax
    new Ajax.Request('/view/fetch_device_popup/' + this.asset);
  }
};

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.clearActivatorTimeout();
    this.clearMenuTimeout();
    var timeout  = this.activator.getAttribute('pivot_timeout') || 100;
    this.activatorTimeout = window.setTimeout(this.show.bind(this), timeout);
    this.activator.observe('mouseout',  this.events.mouseOut);
  },
  mouseOut: function(e) {
    this.clearActivatorTimeout();
    this.activator.stopObserving('mouseout',  this.events.mouseOut);
    this.menuTimeout = window.setTimeout(this.hide.bind(this), 1000);
  },
  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 (!Prototype.Browser.IE) menuTop += 12;
    
    // 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 is throwing strange errors in IE7 around the clonePosition calls, and appears to not be needed for Flash
    // this._renderZIndexFix();

    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);
    document.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._destroyZIndexFix();

    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);
    document.fire("pivot:hidden", this.menu); 
  },
  clearActivatorTimeout: function() {
    if (this.activatorTimeout) window.clearTimeout(this.activatorTimeout);
    this.activatorTimeout = null;
  },

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

  clearMenuTimeout: function() {
    if (this.menuTimeout) window.clearTimeout(this.menuTimeout);
    this.menuTimeout = null;
  },
  
  hasFocus: function(e) {
    document.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.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.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);
    });
  },
  _renderZIndexFix: function(){
    var id = this.menu.id + '_zindex_fix';
    var iframe = $(id);
    if (!iframe) {
      var newIframe = new Element("iframe", {
        id: id,
        style: "position:absolute; -moz-opacity:0; opacity:0; filter:alpha(opacity=0);",
        src: "javascript:void(function(){})();",
        frameborder: 0,
        scrolling: "no"
      });
      newIframe.clonePosition(this.menu, { setTop: (!this.menu.style.height) });
      this.menu.insert({after: newIframe});
      this._renderZIndexFix.bind(this).defer();
      return;
    }
    iframe.clonePosition(this.menu, { setTop: (!this.menu.style.height) });
    iframe.style.zIndex = 499;
    this.menu.style.zIndex = 500;
    this.zIndexFix = iframe;
  },
  _destroyZIndexFix: function(){
    if ( this.zIndexFix && this.zIndexFix.parentNode ){
      this.zIndexFix.parentNode.removeChild( this.zIndexFix );
      this.zIndexFix = null;
    }
  }
};

var PivotManager = {
  active: null,
  initialize: function(){
    if (this.initialized) return;
    if (!this.pivots) this.pivots = $H();
    this._addNew();
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
    
    // 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;
    menuTop = position.top + dimensions.height;

    /* Browser specific pixel-perfect adjustements */
    if (Browser.ie6) {
      menuTop -= 4;
      if (this.multiSelect) menuTop += 7;
    }
    else if (Browser.ie7) {
      menuTop -= 6;
      if (this.multiSelect) menuTop += 7;
    }
    else if (Prototype.Browser.Gecko) {
      menuTop += 6;
      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: '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 is hacky and causes rendering issues, let's not do it
    //this._destroyZIndexFix();

    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', ''));
      
      if (id == item.readAttribute('id')) {
        offset = offset + height + topPadding;
        throw $break;
      }
      else {
        offset = offset + height + topPadding + bottomPadding;
      }
    });
    
    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);
    });
  },
  _renderZIndexFix: function(){
    var id = this.menu.id + '_zindex_fix';
    var iframe = $(id);
    if (!iframe) {
      iframe = new Element('iframe', {id:id, src:'javascript:void(function(){})', frameborder:0,scrolling:'no',style:'style="position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);'});
      this.menu.parentNode.appendChild(iframe);
      window.setTimeout(this._renderZIndexFix.bind(this), 50);
      return;
    }
    iframe.clonePosition(this.menu, {setTop:(!this.menu.style.height)});
    iframe.style.zIndex = 499;
    this.menu.style.zIndex = 500;
    this.zIndexFix = iframe;
  },
  _destroyZIndexFix: function(){
    if ( this.zIndexFix && this.zIndexFix.parentNode ){
      this.zIndexFix.parentNode.removeChild( this.zIndexFix );
      this.zIndexFix = null;
    }
  }
});

var SpiceSelectManager = {
  active: null,
  initialize: function(){
    if ( !this.spiceSelects ) this.spiceSelects = $H();
    this._addNew();
    document.observe('ajax:completed', this.ajaxOnComplete.bindAsEventListener(this));
  },
  ajaxOnComplete: function(){
    this._removeOrphaned();
    this._addNew();
  },
  clearActive: function(){
    if ( this.active ) this.active.hide();
    this.active = null;
  },
  _removeOrphaned: function(){
    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 ShowAssetDetails = {
  initialize: function(){
    $$('div.box h3 a.toggle_content').invoke('observe', 'click', this.toggleBoxClicked.bindAsEventListener(this));
    $$('div.box p.items_shown a').invoke('observe', 'click', this.toggleItemsShownClicked.bindAsEventListener(this));
  },
  toggleBoxClicked: function(event){
    event.stop();
    this.toggleBox(event.element());
  },
  toggleBox: function(link){
    link = $( link );
    var box = link.up('.box');
    var box_content = box.down('.box_content');
    if (box.toggleClassName('collapsed').hasClassName('collapsed')){
      box_content.blindUp( { duration: 0.5 } );
      link.update('Show');
      link.setAttribute('title', 'Expand this section');
    } else {
      box_content.blindDown( { duration: 0.5 } );
      link.update('Hide');
      link.setAttribute('title', 'Hide this section');
    }
  },
  toggleItemsShownClicked: function(event){
    event.stop();
    this.toggleItemsShown(event.element());
  },
  toggleAllBoxes: function(link){
    link = $( link );
    var togglers = $$('div.box h3 a.toggle_content').each(function(link){ 
      this.toggleBox(link); 
    }.bind(this));

    var showing = togglers.first().up('.box').hasClassName('collapsed');

    var text = link;
    if (link.hasClassName('toolbar_button'))  { text =link.down('span.icon'); }
    text.update( showing  ? 'Expand all categories' : 'Hide all categories' );

    link.removeClassName( 'expand' ).removeClassName( 'contract' ).addClassName( showing ? 'expand' : 'contract' );
  },
  toggleAllItemsShown: function(link){
    link = $( link );
    var togglers = $$('div.box p.items_shown a').each(function(link){
      this.toggleItemsShown(link);
    }.bind(this));

    if ( togglers && togglers.size() > 0 ){
      var showing = togglers.first().up('.box').down('table').hasClassName('showing_all'); 

      var text = link;
      if (link.hasClassName('toolbar_button')) { text =link.down('span.icon'); }
      text.update( showing ? 'Collapse all lists' : 'Expand all lists' );

      link.removeClassName( 'expand' ).removeClassName( 'contract' ).addClassName( showing ? 'contract' : 'expand' );
    }
  },
  toggleItemsShown: function(link){
    link = $( link );
    var box_content = link.up('.box_content');
    var meta_text = box_content.down('p.items_shown a span');
    var table = box_content.down('table');
    
    // get only the rows that are initially hidden (index higher than 5)
    var trs = table.select('tbody tr').reject( function(tr, index) { return index < 5; } );
    if (table.toggleClassName('showing_all').hasClassName('showing_all')){
      trs.invoke('show');
      meta_text.update('all');
    } else {
      trs.invoke('hide');
      meta_text.update('5 of');
    }
  }
};

var Rater = Class.create();
Rater.prototype = {
  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 );
    if (!Browser.ff3) this._darkboxFix( darkbox );
    document.fire("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);
    
    document.fire("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(){
      $(darkbox.id + '_iefix').remove();
    }.bind( this ), 1100);
    
    document.fire("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(){ 
      var darkboxFix = $(darkbox.id + '_iefix');
      if (darkboxFix) darkboxFix.remove();
      lightbox.remove();
      darkbox.remove();
    }.bind( this ), ( options.instantDisplay ? 0 : 1100 ) );
    
    document.fire("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';
  },
  _darkboxFix: function( darkbox ){
    darkbox = $(darkbox);
    var darkboxFix = new Element('iframe', {id:darkbox.id + '_iefix', frameborder:0, scrolling:'no', src:'javascript:void(function(){})', style:'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);'});
    darkbox.parentNode.appendChild(darkboxFix);
    setTimeout(function(){
      darkboxFix.setStyle({top:0,left:0,width:darkbox.offsetWidth + 'px',height:darkbox.offsetHeight + 'px'});
      darkbox.style.zIndex = 500;
      darkboxFix.style.zIndex = 499;
    }, 50);
  },
  _fetchFlyover: function(flyoverID){
    new Ajax.Request('/utility/flyover/' + flyoverID);
  }
};

var SoftwareList = {
 
};

var ApplicationOverlay = {
  initialize: function(){
    this.overlay = $('app_overlay');
    this.indicator = $('application_activity_indicator');
    
    if (!this.overlay) return;

    this.nonExcludedActiveRequests = 0; // keep track of ajax requests that triggered the overlay
    
    // url patterns that should not trigger the overlay
    this.exclusionList = $A(['/tickets/periodic_ticket_table_update', 
                             '/help/toggle_visibility', 
                             '/view/auto_complete_for_ticket_attached_to', 
                             '/reports/auto_complete_for_report_filter_value', 
                             '/dashboard/get_widget_content', 
                             '/dashboard/remember_state', 
                             '/settings/monitors/disable_all_email_notifications', 
                             '/dashboard/click_link', 
                             '/ads/blocking_detected', 
                             '/view/fetch_device_popup', 
                             '/inventory/group_summary?chart=', 
                             '/inventory/group_summary?summary_type=hardware&chart=', 
                             '/tickets/set_requires_purchase', 
                             '/search/rebuild_percent_complete', 
                             '/finder/application_polling', 
                             '/settings/categories/resort', 
                             '/dashboard/remember_state', 
                             '/dashboard/increment_app_stat',
                             '/view/hide_trait/',
                             '/ads/detected_resolution',
                             '/settings/monitors/toggle_monitor',
                             '/settings/monitors/toggle_notification' ]);
    
    $('close_app_overlay').observe('click', this.closeOverlayClicked.bindAsEventListener(this));
    
    document.observe('ajax:started', this.ajaxStarted.bindAsEventListener(this));
    document.observe('ajax:completed', this.ajaxCompleted.bindAsEventListener(this));
  },
  closeOverlayClicked: function(event){
    event.stop();
    this._unlock();
  },
  ajaxStarted: function(event){
    var request = event.memo;
    if (!request.options.skipApplicationLocking && this._urlNotExcluded(request.url) && !this.locked) this._lock();
  },
  ajaxCompleted: function(event){
    var request = event.memo;
    if (!request.options.skipApplicationLocking && this._urlNotExcluded(request.url) && this.locked) this._unlock();
  },
  
  _lock: function(){
    this.nonExcludedActiveRequests++;
    this.overlay.show();
    this.indicator.show();
    this.locked = true;
    
    if (this.ajaxTimeout) clearTimeout(this.ajaxTimeout);
    this.ajaxTimeout = setTimeout(this._unlock.bind(this), 20000);
  },
  _unlock: function(){
    if (--this.nonExcludedActiveRequests < 0) this.nonExcludedActiveRequests = 0;
    if (this.nonExcludedActiveRequests == 0){
      this.overlay.hide();
      this.indicator.hide();
      this.locked = false;
    }
  },
  _urlNotExcluded: function(url){ return this.exclusionList.detect( function(element){ return url.indexOf(element) > -1; } ) ? false : true; }
};

Event.register(ApplicationOverlay);

var WebClip = {
  context:null,
  writeTo:null,
  source:null,
  prepare: function( writeTo, source ){
    this.writeTo = $(writeTo);
    this.source = source;
    if (this.writeTo.hide) this.writeTo.setStyle( { visibility:'hidden' } );
  },
  deferredReload: function(context){
    document.observe('dom:loaded', this.reload.curry(context).bindAsEventListener(this));
  },
  reload: function(context){
    this.context = context + ',default';
    this.writeTo.update().setStyle( { visibility:'hidden' } );
    DynamicScriptInclude.load( this.source + '?cobranded=' + Application.cobranded + '&uuid=' + Application.uuid + '&what=' + escape( this.context ), true );
  },
  render: function(htmlSnippet){
    if ( this.context && this.writeTo && htmlSnippet && htmlSnippet != '' ) this.writeTo.update(htmlSnippet).setStyle({ visibility:'visible' });
  },
  hide: function(){ this.writeTo.setStyle( { visibility:'hidden' } ); }
};

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

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();
    DynamicScriptInclude.load( this.source, true );
  },
  render: function( html_snippet ){
    if ( this.writeTo && html_snippet && html_snippet != '' ) this.writeTo.update( html_snippet ).show();
  }
};

var AdHelper = {
  initialize: function(){
    // poll the ad's iframe container, making sure it's always visible. if it's not visible, we assume an adblocker hid the iframe
    if ( $( 'adbox' ) ){
      this.adCheckingInterval = window.setInterval( function(){
        var adframe = $( 'adframe' );
        // if the ads are hidden, show the "Please Try Spiceworks MyWay to get rid of ads" message
        if ( adframe && !adframe.visible() ) this._displayMyWayImage();
      }.bind( this ), 5000 );
    }
  },
  checkResolutionAndRenderAds: function(url){
    var screenWidth = screen.width;
    var resolution = 'normal';
    if (screenWidth && parseInt(screenWidth, 10) < 1200){
      resolution = 'narrow';
      Application.narrowLayout();
    } else url += '&_w=t';

    // render the ads
    var adframe = $('adframe');
    if (adframe) adframe.src = url;

    // wait an arbitrary amount of time, so we're not immediately flooding the browser with ajax requests
    window.setTimeout(function(){
      new Ajax.Request('/ads/detected_resolution', {parameters: {resolution:resolution} });
    }, 3500);
  },
  _displayMyWayImage: function(){
    // clear the polling interval
    if ( this.adCheckingInterval ) window.clearInterval( this.adCheckingInterval );

    // show the "myway" ad if we have the container to update
    var adbox = $( 'adbox' );
    if (adbox){
      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>');
      adbox.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();
    }
  },
  showFullMessage: function( eventID ){
    var brief = $( 'brief_message_for_' + eventID );
    var full = $( 'full_message_for_' + eventID );
    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);
    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 = {
  buildMenu: function(activator, content){
  },
  titleMenuShown: function(pivot){
    if (!pivot || !pivot.activator) return;
    var li = pivot.activator.up('li');
    if (li) pivot.activator.up('li').addClassName('menu-shown');
  },
  titleMenuHidden: function(pivot){
    if (!pivot || !pivot.activator) return;
    var li = pivot.activator.up('li');
    if (li) pivot.activator.up('li').removeClassName('menu-shown');
  },
  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, event){
    if (event && ((event.target && event.target.tagName == 'A') || (event.srcElement && event.srcElement.tagName == 'A'))) return;
    var parent = $(reference);
    if (!parent.hasClassName('generic-activity')) parent = reference.up('li');

    if (parent.hasClassName('open')) parent.removeClassName('open');
    else parent.addClassName('open');
  },
  showAllDetails:function(){ $$('ul.activities > li:not(.open))').invoke('addClassName', 'open'); },
  hideAllDetails:function(){ $$('ul.activities > li.open').invoke('removeClassName', 'open'); },
  
  /* this crap is only for IE6, also known as "only links can super the hover pseudo class" browser */
  mouseOver:function(element){ if (Browser.ie6) $(element).addClassName('hover'); },
  mouseOut:function(element){ if (Browser.ie6) $(element).removeClassName('hover'); },

  /* 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 ActivityElement = {test:function(){}}; /* jp: I removed this because it clearly does nothing, but now it's back again. weird */


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){
     var date = new Date((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 < 0 || day_diff >= 31 )
     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";
     }
};

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