var me;   // hash contains id, name, password, display_name
var lodge;  // hash contains id, name, display_name
var cg_display_name_to_node = new Hash();   // lookup a jid node based on a muc resource (display name)
var portrait_cache_string = (new Date()).getTime();
var photo_base64; // these are used when uploading a new photo, for setting the vcard
var photo_sha1;   // and sending vcard-temp:x:update
var use_rsh = true;     // disable really simple history for safari, because it doesn't work for this app
var cache_page_data = false;    // whether we should reload the data on each render for page objects in the "local_cache" (turn this setting on when xmpp notifications are implemented)
var event_participant_status = new Hash();  // event_id => my current status
var swfu;   // swfupload object
var current_event;  // = the current EventPage object. So that SWFUpload's callbacks can talk to it
var contacts = new Array();  // array of my contacts's names
var contact_node_to_display_name = new Hash();  // saved on roster response, used when handling presence
var unread_notifications = new Hash();   // gets saved onUnload and loaded back onLoad. post_12345 => xml string
var sound_enabled = false;
var last_events_page = 1;    // am i cluttering the global namespace with stuff like this? perhaps should refactor somehow

// Hash that stores stuff we've loaded from the restful backend
// url => object containing json & render method
var local_cache = new Hash();

// Used to display notifications etc. e.g. in Fluid, or play sounds for new messages if inactive
var is_active_window = true;
window.onblur = function() {
  is_active_window = false;
}
window.onfocus = function() {
  is_active_window = true;
  
  if(window.fluid != null) {
    window.fluid.dockBadge = '';
  }
}

// All links in views rendered from the json from the backend use this function
function navigate_to(url, skip_history) {
  // later: check if cache is active or not
  if(local_cache[url] != null) {
    local_cache[url].render();
  } else {
    var obj = match_url(url);
    if(obj == null) {
      //alert('Invalid link :(');
    } else {
      local_cache[url] = obj;
      obj.render();
    }
  }
  if(skip_history == null && use_rsh)
    dhtmlHistory.add(url);
}

// This just returns the object for a page if we want to use that object's data variable
// If it's not in cache it's fetched, but not rendered
function get_page_object(url) {
  if(local_cache[url] != null) {
    return local_cache[url];
  } else {
    var obj = match_url(url);
    local_cache[url] = obj;
    obj.fetch();
    return obj;
  }
}

// this "routes" a url to the correct kind of class for rendering
// readable mappings:
//  /events(?bla=bla)     Events
function match_url(url) {
  if(url.match(/^events\.js(\?.*)?$/)) {
    return new PageEvents(add_lodge_domain(url));
  }
  if(url.match(/^events\/\d+\.js(\?.*)?$/)) {
    return new PageEvent(add_lodge_domain(url));
  }
  if(url.match(/^events\/\d+\/pictures\/\d+\.js(\?.*)?$/)) {
    return new PagePicture(add_lodge_domain(url));
  }
  if(url.match(/^info.js$/)) {
    return new PageInfo('societies/' + lodge.id + '.js');   // just fetch the society resource
  }
  return null;
}

function add_lodge_domain(url) {
  return 'societies/' + lodge.id + '/' + url;
}

// Parent object to different cacheable items
var CachedItem = Class.create({
  initialize: function(url) {
    this.url = url;
    this.data = null;
    this.element = '';
    this.fun = null;
    this.after_render_fun = null;
  },
  render: function(fun, element, after_render_fun) {
    // this method should be overloaded and then call super with a function
    // that does the actual rendering.
    // the after_render_fun can be used e.g. to add event handlers to newly rendered elements
    this.element = element;
    this.fun = fun;
    this.after_render_fun = after_render_fun;
    if(this.data == null || !cache_page_data) {
      this.fetch(this.on_complete_render);
    } else {
      $(element).update(fun());
      // show correct tab
      $$('.tab_content').invoke('hide');
      $(element).show();
      $(element).scrollIntoView();
      $$('.tab_button').invoke('setStyle', {background: "url('/images/regular.png')"});
      $$('.tab_button.first').invoke('setStyle', {background: "url('/images/first.png')"});
      $(element + '_button').setStyle({background: "url('/images/regular-selected.png')"})
      
      if(this.after_render_fun != null)
        this.after_render_fun();
    }
  },
  fetch: function(complete_fun) {
    // please note: this only fetches the data and puts it in the data var
    // the oncomplete function doesn't do anything else, so don't call this function and directly
    // use the data variable, as there will be timing issues.
    // can only safely be used when rendering something else, to fetch something that will be used
    // when the user interacts with what has been rendered (but always check for data == null in
    // case it hasn't been fetched yet)
    if(complete_fun == null)
      complete_fun = this.on_complete_store;
    new Ajax.Request('/' + this.url, {
      asynchronous: true,
      evalScripts: false,
      method: 'get',
      onComplete: complete_fun.bindAsEventListener(this)
    });
  },
  on_complete_render: function(request) {
    this.data = request.responseText.evalJSON();
    $(this.element).update(this.fun());
    // show correct tab
    $$('.tab_content').invoke('hide');
    $(this.element).show();
    $(this.element).firstChild.scrollIntoView();
    $$('.tab_button').invoke('setStyle', {background: "url('/images/regular.png')"});
    $$('.tab_button.first').invoke('setStyle', {background: "url('/images/first.png')"});
    $(this.element + '_button').setStyle({background: "url('/images/regular-selected.png')"})
    
    if(this.after_render_fun != null)
      this.after_render_fun();
  },
  on_complete_store: function(request) {
    this.data = request.responseText.evalJSON();
  }
});

function a_to(url) {
  return '<a href="#" onclick="navigate_to(\'' + url + '\'); return false;">';
}

// Chat page, special case
var page_lounge = {
  render: function() {
    // fetch nothing, lounge always stays the same through a session
    $$('.tab_content').invoke('hide');
    $('lounge').show();
    
    $$('.tab_button').invoke('setStyle', {background: "url('/images/regular.png')"});
    $('lounge_button').setStyle({background: "url('/images/first-selected.png')"})
    
  	$('cg_messages').lastChild.scrollIntoView();
  	if(dhtmlHistory.isSafari)
  	  $('container').scrollIntoView();   // safari's scrollIntoView implementation is buggy    
  }
}

// Holds Events index
var PageEvents = Class.create(CachedItem, {
  // because of the asynch nature of the ajax request, we must send the update function there
  render: function($super) {
    var fun = function() {
      var html = '';
      html += '<div id="all_events">';
      html += ' <div id="events_sidebar">';
      html += '  <div>';
      html += '   <a href="#" id="new_event_link" onclick="$(\'new_event\').show(); $(\'new_event_link\').hide(); return false;">Ny träff ffs!!!</a>';
      html += '   <form id="new_event" style="display: none; margin-bottom: 30px;">';
      html += '    <b>Ny träff</b><br />';
      html += '    <input type="hidden" name="event[society_id]" value="' + lodge.id + '" />';
      html += '    Vad? (titel)<br />';
      html += '    <input type="text" name="event[title]" id="new_event_title" style="width: 215px; margin-bottom: 5px;" /><br />';
      html += '    Var?<br />';
      html += '    <input type="text" name="event[location]" id="new_event_location" style="width: 215px; margin-bottom: 5px;" /><br />';
      html += '    När?<br />';
      html += '    <label for="new_event_start_date" style="display: block; float: left; width: 63px;">Dag:</label> <input type="text" name="start_date" id="new_event_start_date" style="width: 100px; margin-bottom: 2px;" /><br />';
      html += '    <label for="new_event_start_time" style="display: block; float: left; width: 63px;">Tid:</label> <input type="text" name="start_time" id="new_event_start_time" value="12:00"/> - ';
      html += '    <input type="text" name="end_time" id="new_event_end_time" value="16:00"/><br />';
      html += '    <a href="#" id="show_end_date_picker" onclick="$(\'end_date_picker\').show(); $(\'show_end_date_picker\').hide(); return false;" style="display: block; margin-bottom: 5px;">välj annan slutdag</a>';
      html += '    <span id="end_date_picker" style="display: none;"><label for="new_event_start_date" style="display: block; float: left; width: 63px;">Slutdag:</label> <input type="text" name="end_date" id="new_event_end_date" style="width: 100px; margin-top: 2px; margin-bottom: 5px;" /><br /></span>';
      html += '    <span style="margin-top: 7px;">Hur? (beskrivning)</span><br />';
      html += '    <textarea name="event[description_raw]" id="new_event_description" style="width: 215px; height: 80px; margin-bottom: 5px;"></textarea><br />';
      html += '    <input type="submit" value="OK!" /> eller <a href="#" onclick="$(\'new_event_link\').show(); $(\'new_event\').hide(); return false;">avbryt</a>';
      html += '   </form>';
      html += '  </div>';
      html += '  <div>Mest besökta eventen, tjipp å tjopp.</div>';
      html += ' </div>';
      
      html += ' <div id="events_content">';
      html += '  <div id="upcoming_events">';
      var todays_date = new Date();
      var upcoming_events = this.data.events.reject(function(ev) {
        var d = new Date();
        d.setISO8601(ev.start_date);
        return d < todays_date;
      });
      upcoming_events.each(function(ev) {
        html += this.render_upcoming_event(ev);
      }.bind(this));
      html += '  </div>';
      
      html += '  <div id="past_events">';
      var past_events = this.data.events.reject(function(ev) {
        var d = new Date();
        d.setISO8601(ev.start_date);
        return d >= todays_date;
      });
      past_events.each(function(ev) {
        html += this.render_past_event(ev);
      }.bind(this));
      html += '  </div>';
      
      html += '  <div id="events_pages" style="width: ' + ((this.data.events_total / 12.0).ceil() * 41) + 'px;">';
      var this_page = this.data.page;
      (this.data.events_total / 12.0).ceil().times(function(i) {
        html += '   <div' + ((i + 1 == this_page) ? ' style="background: #ddd;"' : '') + '>' + a_to('events.js?page=' + (i + 1)) + (i + 1) + '</a></div>';
      });
      html += '   <div class="clearing_div" style="width: 0px; height: 0px; border: none; visibility: hidden;"></div>';
      html += '  </div>';
      html += '  <div style="clear: both;"></div>'
      html += ' </div>';
      html += '</div>';
      
      return html;
    };
    
    var after_render_fun = function() {
      var dpf = new DatePickerFormatter(['yyyy', 'mm', 'dd'], '-');
      var dpck_options = $H({
        language: 'sv',
        dateFormat: dpf,
        showDuration: 0.2,
        closeEffectDuration: 0.2,
        disablePastDate: true,
        disableFutureDate: false,
        enableCloseOnBlur: true
      });
      
      var dpck = new DatePicker(dpck_options.merge({relative: 'new_event_start_date'}).toObject());
      var dpck2 = new DatePicker(dpck_options.merge({relative: 'new_event_end_date'}).toObject());
      
      var d = new Date();
      $('new_event_start_date').value = dpf.date_to_string(d.getFullYear(), d.getMonth() + 1, d.getDate() + 3);
      
      $('new_event').observe('submit', this.submit_new_event.bindAsEventListener(this));
      
      // used to navigate back from event to previous page
      last_events_page = this.data.page;
    }
    
    $super(fun.bind(this), 'events', after_render_fun.bind(this));
  },
  submit_new_event: function(event) {
    Event.stop(event);
    if($('new_event_title').value.strip() == '')
      return;
    if($('new_event_end_date').value.strip() == '')
      $('new_event_end_date').value = $('new_event_start_date').value;
    
    new Ajax.Request('/societies/' + lodge.id + '/events.js', {
      asynchronous: true,
      evalScripts: false,
      parameters: Form.serialize($('new_event')),
      method: 'post',
      onSuccess: function(request) {
        var ev = request.responseText.evalJSON();
        var html = this.render_upcoming_event(ev);
        // TODO: put in correct order by start date.
        $('upcoming_events').insert({top: html});
        new Effect.Highlight($('upcoming_events').firstChild);
      }.bind(this),
      onFailure: function(request) {
        //alert('FAIL!');
        var error_msg = '';
        var errors = request.responseText;
        errors.split(',').each(function(err) {
          if(err == 'start_date is not present')
            error_msg += 'Du måste ange ett startdatum\n';
          if(err == 'end_date is not present')
            error_msg += 'Du måste ange ett slutdatum\n';
          if(err == 'start_date must be before end date')
            error_msg += 'Startdatumet måste vara före slutdatumet\n';
        });
        alert('OMG FEL!!!\n\n' + error_msg);
      }
    });
  },
  render_past_event: function(ev) {
    var html = '';
    html += '<div class="event">';
    html += ' <img src="/images/pictures/' + ev.thumbnail_id + '_tn.jpeg" style="float: left;" /> ' + a_to('events/' + ev.id + '.js') + '<h1>' + ev.title + ' (' + (ev.participants[0] || 0) + ')</h1></a>';
    html += ' <p>' + time_nice_short(ev.start_date, true, true) + (ev.location == null ? '' : ', ' + ev.location) + '. Skapad av ' + ev.member_display_name + '.</p>';
    html += ' <p style="color: #444;">';
    html += ' ' + ev.description.stripTags().truncate(200) + '</p>';
    html += '</div>';
    return html;
  },
  render_upcoming_event: function(ev) {
    var html = '';
    html += '<div class="event">';
    html += ' ' + a_to('events/' + ev.id + '.js') + '<h1>' + ev.title + '</h1></a>';
    html += ' <p>Startar: ' + time_nice_short(ev.start_date, true, true) + (ev.location == null ? '' : ', ' + ev.location) + '. Skapad av ' + ev.member_display_name + '.</p>';
    html += ' <p style="color: #444;">' + ev.description.stripTags().truncate(200) + '</p>';
    html += ' <p>';
    html += '  Tänker du komma?';
    var my_status = event_participant_status[ev.id];
    var link_texts = ['Ja', 'Nej', 'Kanske'];
    (3).times(function(n) {
      html += '  <a id="participant_choice_' + ev.id + '_' + (n+1) + '" class="participant_choice_' + ev.id + ' ' + ((my_status == n+1) ? 'active_participant_choice' : '') + '" href="#" onclick="new Ajax.Request(\'/societies/' + lodge.id + '/events/' + ev.id + '/set_participant_status.js\', {asynchronous: true, evalScripts: false, parameters: {status: ' + (n+1) + '}, method: \'post\', onSuccess: function(request) { event_participant_status[' + ev.id + '] = ' + (n+1) + '; if($$(\'.participant_choice_' + ev.id + '.active_participant_choice\').size()) { $$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML = parseInt($$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML) - 1;} $$(\'.participant_choice_' + ev.id + '\').invoke(\'removeClassName\', \'active_participant_choice\'); $(\'participant_choice_' + ev.id + '_' + (n+1) + '\').addClassName(\'active_participant_choice\'); $$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML = parseInt($$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML) + 1; }, onFailure: function(request) { alert(\'FAIL!\'); }}); return false;">' + link_texts[n] + ' (' + '<span>' + (ev.participants[n+1] || 0) + '</span>' + ')</a> ';
    });
    html += '</p>';
    html += '</div>';
    return html;
  }
});

// One event
var PageEvent = Class.create(CachedItem, {
  render: function($super) {
    var fun = function() {
      var ev = this.data;
      
      var todays_date = new Date();
      var d = new Date();
      d.setISO8601(ev.start_date);
      var upcoming_event = d > todays_date;
      
      var html = '';
      html += '<div style="position: absolute; width: 100%;">';
      html += ' <div id="event_sidebar">';
      html += '  <p style="margin: 10px; font-size: 14pt;">';
      //html += '   <span class="event_show">' + time_nice_short(ev.start_date) + '</span>';
      //html += '   <span class="event_edit" style="display: none;">Välj tid osv.</span>';
      // don't allow changing the start time for now -- confusing, just stick to the friggin' time
      html += '   <span>' + time_nice_short(ev.start_date) + '</span>';
      html += '  </p>';
      html += '  <p style="margin: 10px; font-size: 14pt;">';
      html += '   <span id="event_location" class="event_show">' + (ev.location == null ? '' : ev.location) + '</span>';
      html += '   <span class="event_edit" style="display: none;"><input type="text" id="edit_event_location" value="' + (ev.location == null ? '' : ev.location) + '" /></span>';
      html += '  </p>';
      
      html += '  <div class="participants" style="margin: 10px;">';
      var part_list = [];
      var yes_part_list = [];
      var no_part_list = [];
      var maybe_part_list = [];
      ev.participants.each(function(part) {
        // by ignoring ourselves we don't have to update the list when we change our rsvp status... clever eh?
        // ignore that condition for past events
        if(part.name != me.name || !upcoming_event) {
          var part_item = '<div style="clear: both; line-height: 32px;">';
          part_item += '<img src="/images/portraits/' + part.name + '_tiny.png?' + portrait_cache_string + '" style="margin: 3px; float: left; border: 1px solid;"> <a href="http://' + part.name + '.tsargustaf.com" target="_blank">' + part.display_name + '</a>';
          part_item += '</div>';
          if(part.status == 0) {
            part_list.push(part_item);  // for past events, status 0 = was there
          } else if(part.status == 1) {
            yes_part_list.push(part_item);
          } else if(part.status == 2) {
            no_part_list.push(part_item);
          } else if(part.status == 3) {
            maybe_part_list.push(part_item);
          }          
        }
      });
      if(part_list.size() != 0) {
        html += '   <div style="clear: both;">Närvarande: ' + part_list.join('') + '</div>';
      }
      if(yes_part_list.size() != 0) {
        html += '   <div style="clear: both;">Kommer: ' + yes_part_list.join('') + '</div>';
      }
      if(no_part_list.size() != 0) {
        html += '   <div style="clear: both;">Kommer inte: ' + no_part_list.join('') + '</div>';
      }
      if(maybe_part_list.size() != 0) {
        html += '   <div style="clear: both;">Kanske kommer: ' + maybe_part_list.join('') + '</div>';
      }
      html += '  </div>';
      
      if(upcoming_event) {
        // This is an upcoming event -- show "are you coming"
        var part_counts = [];
        part_counts[1] = yes_part_list.size();
        part_counts[2] = no_part_list.size();
        part_counts[3] = maybe_part_list.size();
        
        html += '  <p>';
        html += '   Tänker du komma?<br />';    // Répondez s'il vous plaît
        var my_status = event_participant_status[ev.id];
        part_counts[my_status]++;
        var link_texts = ['Ja', 'Nej', 'Kanske'];
        (3).times(function(n) {
          html += '   <a id="participant_choice_' + ev.id + '_' + (n+1) + '" class="participant_choice_' + ev.id + ' ' + ((my_status == n+1) ? 'active_participant_choice' : '') + '" href="#" onclick="new Ajax.Request(\'/societies/' + lodge.id + '/events/' + ev.id + '/set_participant_status.js\', {asynchronous: true, evalScripts: false, parameters: {status: ' + (n+1) + '}, method: \'post\', onSuccess: function(request) { event_participant_status[' + ev.id + '] = ' + (n+1) + '; if($$(\'.participant_choice_' + ev.id + '.active_participant_choice\').size()) { $$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML = parseInt($$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML) - 1;} $$(\'.participant_choice_' + ev.id + '\').invoke(\'removeClassName\', \'active_participant_choice\'); $(\'participant_choice_' + ev.id + '_' + (n+1) + '\').addClassName(\'active_participant_choice\'); $$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML = parseInt($$(\'.participant_choice_' + ev.id + '.active_participant_choice span\').first().innerHTML) + 1; }, onFailure: function(request) { alert(\'FAIL!\'); }}); return false;">' + link_texts[n] + ' (' + '<span>' + (part_counts[n+1] || 0) + '</span>' + ')</a> ';
        });
        html += '  </p>';        
      }
      
      if(ev.member_id == me.id || lodge.is_admin) {
        html += '  <p><a href="#" onclick="$$(\'.event_show\').invoke(\'hide\'); $$(\'.event_edit\').invoke(\'show\'); return false;">Redigera denna träff</a></p>'
      }
      html += '  <div class="clearing_div"></div>';
      html += ' </div>';
      
      html += ' <div id="event_content">';
      html += '  <h1>';
      html += '   ' + a_to('events.js?page=' + last_events_page) + '&larr;</a>';
      html += '   <span id="event_title" class="event_show">' + ev.title + '</span>';
      html += '   <span class="event_edit" style="display: none;"><input type="text" id="edit_event_title" style="font-size: 20pt; font-family: georgia;" value="' + ev.title + '" /></span>';
      html += '  </h1>';
      html += '  <div class="event_description">';
      html += '   <div id="event_description" class="event_show">' + ev.description + '</div>';
      html += '   <div class="event_edit" style="display: none;"><textarea id="edit_event_description" style="width: 450px; color: #444; font-family: Georgia;" rows="10">' + ev.description_raw + '</textarea></div>';
      html += '   <div class="clearing_div"></div>';
      html += '  </div>';
      
      html += '  <div class="event_edit" style="display: none;">';
      html += '   <input type="button" id="update_event" value="Spara" />';
      html += '   eller <a href="#" onclick="$$(\'.event_edit\').invoke(\'hide\'); $$(\'.event_show\').invoke(\'show\'); $(\'picture_tab\').hide(); return false;">avbryt</a>';
      html += '  </div>';
      
      html += '  <div class="event_tab_buttons_container event_show">';
      html += '   <div class="event_tab_buttons">';
      html += '    <a id="comment_tab_button" href="#" onclick="$$(\'.event_tabs\').invoke(\'hide\'); $(\'comment_tab\').show(); $(\'picture_tab_button\').setStyle({background: \'none\'}); $(\'comment_tab_button\').setStyle({background: \'url(/images/simpletabs-selected.png)\'}); return false;" style="background: url(\'/images/simpletabs-selected.png\');">Diskussion (' + ev.comments.size() + ')</a>';
      html += '    <a id="picture_tab_button" href="#" onclick="$$(\'.event_tabs\').invoke(\'hide\'); $(\'picture_tab\').show(); $(\'comment_tab_button\').setStyle({background: \'none\'}); $(\'picture_tab_button\').setStyle({background: \'url(/images/simpletabs-selected.png) -130px\'}); return false;">Bilder (' + ev.pictures.size() + ')</a>';
      html += '   </div>';
      html += '  </div>';

      html += '  <div class="event_tabs event_show" id="picture_tab" style="display: none;">';
      html += '   <div class="pictures" style="width: 450px; margin: 0px auto;">';
      ev.pictures.each(function(pic) {
        html += a_to('events/' + ev.id + '/pictures/' + pic.id + '.js') + '<img src="/images/pictures/' + pic.id + '_tn.jpeg" style="float: left;" /></a>';
      });
      html += '    <div style="clear: both;"></div>';
      html += '   </div>';
      
      html += '   <div style="clear: both; margin-top: 10px;">';
      html += '    <span style="font-size: 14pt;">Ladda upp bilder!</span>';
      if(FlashDetect.majorAtLeast(9)) {
      	html += '    <form id="form1" action="index.php" method="post" enctype="multipart/form-data">';
  			html += '     <fieldset class="flash" id="fsUploadProgress">';
  			html += '     </fieldset>';
  			html += '     <p id="divStatus">0 bilder uppladdade</p>';
      	html += '     <p>';
      	html += '      <input id="btnBrowse" type="button" value="Ladda upp bilder" onclick="swfu.selectFiles();" style="font-size: 8pt;" />';
      	html += '      <input id="btnCancel" type="button" value="Avbryt alla uppladdningar" onclick="swfu.cancelQueue();" disabled="disabled" style="font-size: 8pt;" />';
      	html += '     </p>';
      	html += '    </form>';
        html += '   </div>';
        html += '   <div id="publish_pics" style="display: none;">';
        html += '    <form action="/societies/' + lodge.id + '/events/' + ev.id + '/pictures/publish" method="post" onsubmit="new Ajax.Request(\'/societies/' + lodge.id + '/events/' + ev.id + '/pictures/publish.js\', {asynchronous:true, method:\'put\', evalScripts:true, parameters:Form.serialize(this), onComplete: function() {$(\'unpublished_pictures\').update(\'\'); $(\'publish_submit\').hide();} }); return false;">';
    		html += '     <div id="unpublished_pictures">';
        html += '      <span style="font-size: 14pt;">Bilder som du laddat upp men inte skrivit beskrivning till ännu</span>';
    		html += '     </div>';
        html += '     <div id="publish_submit">';
    		html += '      <input type="submit" value="Spara"/>';
    		html += '     </div>';
    		html += '    </form>';
    		html += '   </div>';
    	} else {
    	  html += '   Flash version 9 eller senare krävs för att ladda upp bilder';
    	}
  		html += '  </div>';
      
      html += '  <div class="event_tabs event_show" id="comment_tab">';
      html += '   <div id="comments">';
      ev.comments.each(function(c) {
        html += this.render_comment(c);
      }.bind(this));
      html += '   </div>';
    	html += '   <div class="new_comment">';
    	html += '    Skriv ny kommentar:';
    	html += '    <form id="new_comment">';
    	html += '	    <textarea class="comment_area" id="new_comment_text" name="event_comment[text_raw]" style="font: inherit; color: #444; margin: 5px 0px;"></textarea>';
    	html += '     <input class="comment_submit" type="submit" value="POSTA!" />';
    	html += '    </form>';
    	html += '   </div>';
      html += '  </div>';

      html += ' </div>';
      html += '</div>';
      
      current_event = this;
      if(FlashDetect.majorAtLeast(9)) {
        swfu.setUploadURL('/societies/' + lodge.id + '/events/' + ev.id + '/pictures.js');
      }
      
      return html;
    };
    
    var after_render_fun = function() {
      $('new_comment').observe('submit', this.submit_new_comment.bindAsEventListener(this));
      $('update_event').observe('click', this.update_event.bindAsEventListener(this));
      this.show_unpublished_pictures();  // show unpublished pics from earlier sessions
    }
    
    $super(fun.bind(this), 'events', after_render_fun.bind(this));
  },
  update_event: function() {
  	new Ajax.Request('/societies/' + lodge.id + '/events/' + this.data.id + '.js',
  		{ asynchronous: true,
  			evalScripts: true,
  			method: 'put',
  			parameters:
  			  { 'event[title]': $('edit_event_title').value,
  			    'event[description_raw]': $('edit_event_description').value,
  			    'event[location]': $('edit_event_location').value
  			  },
  			onComplete: function(request) {
        	var json = request.responseText.evalJSON();
        	$('event_title').update(json.title);
        	$('event_description').update(json.description);
        	$('event_location').update(json.location);
          $$('.event_edit').invoke('hide');
          $$('.event_show').invoke('show');
          $('picture_tab').hide();
  			}
  		}
  	);    
  },
  show_unpublished_pictures: function() {
  	new Ajax.Request('/societies/' + lodge.id + '/events/' + this.data.id + '/pictures/unpublished.js',
  		{ asynchronous: true,
  			evalScripts: true,
  			method: 'get',
  			onComplete: function(request) {
        	var json = request.responseText.evalJSON();
        	var render_fun = this.render_unpublished_picture;
        	json.each(function(pic) {
        		if(!$('unpublished_picture_' + pic.id)) {		// ensure each picture is only listed once
        			$('unpublished_pictures').insert(render_fun(pic));
        		}
        	});
          
        	if(json.length > 0) {
        		$('publish_pics').show();
        	}
  			}.bind(this)
  		}
  	);
  },
  render_unpublished_picture: function(pic) {
  	var pic_orig_created;
  	if(pic.orig_created == null) {
  		pic_orig_created = '';
  	} else {
  	  var d = new Date();
  	  d.setISO8601(pic.orig_created);
  		pic_orig_created = d.toDatabaseString();
  	}
    
  	var html = [
  		'<div class="unpublished_picture" id="unpublished_picture_' + pic.id + '">',
  			'<img src="/images/pictures/' + pic.id + '_tn.jpeg" />',
  			'<div class="unpublished_picture_input">',
  				'Beskrivning: <textarea name="desc[' + pic.id + ']"></textarea>',
  				'Bilden tagen: <input type="text" name="orig_created[' + pic.id + ']" value="' + pic_orig_created + '" />',
  			'</div>',
  		'</div>'
  	];
    
  	return html.join('');
  },
  submit_new_comment: function(event) {
    Event.stop(event);
    if($('new_comment_text').value.strip() == '')
      return;
    new Ajax.Request('/societies/' + lodge.id + '/events/' + this.data.id + '/comments.js', {
      asynchronous: true,
      evalScripts: false,
      parameters: Form.serialize($('new_comment')),
      method: 'post',
      onSuccess: function(request) {
        var c = request.responseText.evalJSON();
        var html = this.render_comment(c);
        $('comments').insert(html);
        $('comments').lastChild.scrollIntoView();
        new Effect.Highlight($('comments').lastChild);
        $('new_comment_text').value = '';
      }.bind(this),
      onFailure: function(request) {
        alert('FAIL!');
        // do something that makes sense        
      }
    });
  },
  render_comment: function(c) {
    if(c.member == null) {
      c.member = {name: '', id: 0, display_name: '<i>Borttagen</i>'};
    }
    var html = '';
    
    html += '<div id="comment_' + c.id + '" class="comment">';
  	html += ' <div class="comment_member_pic"><img src="/images/portraits/' + c.member.name + '_small.png" alt="Portrait"/></div>';
  	html += '	<div class="comment_member_name"><a href="http://' + c.member.name + '.tsargustaf.com" target="_blank">' + c.member.display_name + '</a> (' + time_nice_short(c.created_at) + ')</div>';
  	html += '	<div class="comment_text">' + c.text + '</div>';
  	if(c.member.id == me.id) {
  	  // remove button
  	}
  	html += '	<div class="clearing_div"></div>';
  	html += '</div>';
    return html;
  }
});

// One picture
var PagePicture = Class.create(CachedItem, {
  render: function($super) {
    var fun = function() {
      var pic = this.data;
      
      // enable flicking through the pictures in this event
      var a = this.url.split('/');      // this.url = "societies/z/events/x/pictures/y.js"
      this.event_obj = get_page_object(a[2] + '/' + a[3] + '.js');   // "events/x.js"
      
      var html = '';
      html += '<div id="picture">';
      html += ' <div id="picture_sidebar">';
      html += '  <div>Tagen: ' + time_nice_short(pic.orig_created) + '.<br/>Kamera: ' + pic.camera + '.<br />Uppladdad: ' + time_nice_short(pic.created_at) + '.</div>';
      
      html += '  <div style="width: 180px; margin: 10px auto;">';
      var pic_ids = this.event_obj.data.pictures.pluck('id');
      var this_pic_index = pic_ids.indexOf(this.data.id);
      html += '   Bläddra (' + (this_pic_index + 1) + '/' + pic_ids.size() + ')';
      if(this_pic_index - 1 < 0) {
        html += '<div style="float: left; width: 90px; height: 90px; background: #fff;">Detta är första bilden</div>';        
      } else {
        html += '<div style="float: left; width: 90px; height: 90px;">' + a_to('events/' + this.event_obj.data.id + '/pictures/' + pic_ids[this_pic_index - 1] + '.js') + '<img src="/images/pictures/' + pic_ids[this_pic_index - 1] + '_tn.jpeg" /></a></div>';
      }
      
      if(this_pic_index + 1 >= pic_ids.size()) {
        html += '<div style="float: left; width: 90px; height: 90px; background: #fff;">Detta är sista bilden</div>';
      } else {
        html += '<div style="float: left; width: 90px; height: 90px;">' + a_to('events/' + this.event_obj.data.id + '/pictures/' + pic_ids[this_pic_index + 1] + '.js') + '<img src="/images/pictures/' + pic_ids[this_pic_index + 1] + '_tn.jpeg" /></a></div>';
      }
      html += '  <div class="clearing_div"></div>';
      html += '  </div>';
      
      if(this.event_obj.data.member_id == me.id) {
        html += '  <div><button id="set_as_thumb" onclick="new Ajax.Request(\'/societies/' + lodge.id + '/events/' + this.event_obj.data.id + '/set_thumbnail.js\', {asynchronous: true, parameters: {thumb_id: ' + pic.id + '}, method: \'post\'});">Sätt som thumbnail</button></div>';
      }
      html += ' </div>';
      html += ' <div id="picture_content">';
      html += '  <h1>Bilder från ' + a_to(a[2] + '/' + a[3] + '.js') + this.event_obj.data.title + '</a></h1>';
      
      html += '  <div style="text-align: center; margin-right: 5px; overflow: hidden;">';
      if(this_pic_index + 1 < pic_ids.size())
        html += a_to('events/' + this.event_obj.data.id + '/pictures/' + pic_ids[this_pic_index + 1] + '.js');
      html += '   <img src="/images/pictures/' + pic.id + '.jpeg" style="max-width: 535px; max-height: 535px;" />';
      if(this_pic_index + 1 < pic_ids.size())
        html += '</a>';
      html += '  </div>';
      
      html += '  <div id="pic_description">' + pic.description + '</div>';
      
      html += '  <div id="comment_pane">';
      html += '   <div id="comments">';
      pic.comments.each(function(c) {
        html += this.render_comment(c);
      }.bind(this));
      html += '   </div>';
    	html += '   <div class="new_comment">';
    	html += '    Skriv ny kommentar:';
    	html += '    <form id="new_comment">';
    	html += '	    <textarea class="comment_area" id="new_comment_text" name="picture_comment[text_raw]" style="font: inherit; color: #444; margin: 5px 0px;"></textarea>';
    	html += '     <input class="comment_submit" type="submit" value="POSTA!" />';
    	html += '    </form>';
    	html += '   </div>';
    	html += '  </div>';
      
      
      // html += '  <div id="comments">';
      // html += '   <div>';
      // html += '    <form id="new_comment">';
      // html += '     <textarea name="picture_comment[text_raw]" id="new_comment_text"></textarea>';
      // html += '     <input type="submit" value="Säg det!" />';
      // html += '    </form>';
      // html += '   </div>';
      // pic.comments.each(function(c) {
      //   html += this.render_comment(c);
      // }.bind(this));
      // html += '  </div>';
      // html += ' </div>';
      // html += '</div>';
      return html;
    };
    
    var after_render_fun = function() {
      // Now it's rendered, let's add event handlers
      $('new_comment').observe('submit', this.submit_new_comment.bindAsEventListener(this));
    }
    
    $super(fun.bind(this), 'events', after_render_fun.bind(this));
  },
  submit_new_comment: function(event) {
    Event.stop(event);
    if($('new_comment_text').value.strip() == '')
      return;
    new Ajax.Request('/societies/' + lodge.id + '/events/' + this.event_obj.data.id + '/pictures/' + this.data.id + '/comments.js', {
      asynchronous: true,
      evalScripts: false,
      parameters: Form.serialize($('new_comment')),
      method: 'post',
      onSuccess: function(request) {
        var c = request.responseText.evalJSON();
        var html = this.render_comment(c);
        $('comments').insert(html);
        $('comments').lastChild.scrollIntoView();
        new Effect.Highlight($('comments').lastChild);
        $('new_comment_text').value = '';
      }.bind(this),
      onFailure: function(request) {
        alert('FAIL!');
        // do something that makes sense        
      }
    });
  },
  render_comment: function(c) {
    if(c.member == null) {
      c.member = {name: '', id: 0, display_name: '<i>Borttagen</i>'};
    }
    var html = '';
    
    html += '<div id="comment_' + c.id + '" class="comment">';
  	html += ' <div class="comment_member_pic"><img src="/images/portraits/' + c.member.name + '_small.png" alt="Portrait"/></div>';
   	html += '	<div class="comment_member_name"><a href="http://' + c.member.name + '.tsargustaf.com" target="_blank">' + c.member.display_name + '</a> (' + time_nice_short(c.created_at) + ')</div>';
  	html += '	<div class="comment_text">' + c.text + '</div>';
  	if(c.member.id == me.id) {
  	  // remove button
  	}
  	html += '	<div class="clearing_div"></div>';
  	html += '</div>';
    return html;
    
    // if(c.member == null) {
    //   c.member = {name: '', id: 0, display_name: '<i>Borttagen</i>'};
    // }
    // var html = '';
    // html += '<div id="comment_' + c.id + '" style="clear: both; margin-bottom: 5px;">';
    // html += ' <img src="/images/portraits/' + c.member.name + '_small.png" style="float: left;" />';
    // html += ' <b>' + c.member.display_name + '</b>: (' + time_nice_short(c.created_at) + ')';
    // if(c.member.id == me.id) {
    //   html += ' <a href="#" onclick="if(confirm(\'Är du säker på att du vill ta bort kommentaren?\')) { new Ajax.Request(\'/societies/' + lodge.id + '/events/' + this.event_obj.data.id + '/pictures/' + this.data.id + '/comments/' + c.id + '.js\', {asynchronous:true, method:\'delete\', evalScripts:false, onSuccess: function(request) { $(\'comment_' + c.id + '\').remove(); }}); } return false;">ta bort</a>';
    // }
    // html += '<br />';
    // html += ' ' + c.text;
    // html += '</div>';
    // return html;
  }
});

// Info
var PageInfo = Class.create(CachedItem, {
  render: function($super) {
    var fun = function() {
      var soc = this.data;
      
      var html = '';
      html += '<div id="society_info">';
      html += ' <div id="info_sidebar">';
      html += '  <img src="http://maps.google.com/staticmap?zoom=3&markers=' + soc.lat + ',' + soc.long + '&size=230x230&key=ABQIAAAAPyIiI57uH0Hj0PdCfnvNehQ9krqjpjcSMuoS3vNV926I2FgOdhQDR_5Fm73W-9ZnRNpZPkrdXwaGxQ" alt="Karta" style="height: 230px; width: 230px; margin: 5px 2px;" />';
      html += '  <div style="margin: 10px;">';
      html += '   <div style="clear: both;">Administratörer</div>';
      soc.members.each(function(m) {
        if(m.admin > 0) {
          html += '<div style="clear: both; line-height: 32px;">';
          html += '  <img src="/images/portraits/' + m.name + '_tiny.png?' + portrait_cache_string + '" style="margin: 3px; float: left; border: 1px solid;"> <a href="http://' + m.name + '.tsargustaf.com" target="_blank">' + m.display_name + '</a>';
          html += '</div>';
        }
      });
      html += '  </div>';

      html += ' </div>';
      html += ' <div id="info_content">';
      html += '  <img src="/images/' + soc.id + '.png" alt="Logelogotyp" style="float: left; margin: 10px;" />';
      html += '  <h1 style="margin: 10px;">' + soc.display_name + '</h1>';
      html += '  ' + soc.presentation;
      html += ' </div>';
      html += '</div>';
      return html;
    };
    
    $super(fun.bind(this), 'info');
  }
});

function initialize() {
  if(dhtmlHistory.isSafari) {
    use_rsh = false;    // rsh in safari just doesn't work for this app.. maybe in 0.8?
    // safari clients are just logged out when there is too much inactivity, so start a timeout interval
    // that sends a ping to the server every 5 mins in order to keep the connection alive
    // try to remove this in newer version of JSJaC
    setInterval("xmpp_ping()", 300000);
  }
  
  // works fine in FF, IE not tested yet
  
  local_cache['lounge'] = page_lounge;
  
  if(use_rsh) {
    dhtmlHistory.initialize();
    dhtmlHistory.addListener(yourListener);
    
    // so we can back-button to the lounge
    dhtmlHistory.add('lounge');
  }
  
	new Ajax.Request('/main/load_data.js',
		{ asynchronous: false,    // this needs to be in synch because rest of init is dependant on the global vars we fetch
			evalScripts: true,
			method: 'get',
			onComplete: function(request) {
				init_data_loaded(request.responseText.evalJSON());
			}
		}
	);
}

function xmpp_ping() {
  // ejabberd actually doesn't support XMPP ping yet, but we'll receive an error response until it does
  var iq = new JSJaCIQ();
  iq.setIQ('tsargustaf.com', 'get', 'ping' + (new Date()).getTime());
  iq.appendNode('ping', {xmlns: 'urn:xmpp:ping'});
  con.send(iq);
}

function init_data_loaded(json) {
  me = {id: json.id, name: json.name, password: json.password, display_name: json.display_name};
  lodge = {id: json.lodge_id, name: json.lodge_name, display_name: json.lodge_display_name, is_admin: json.is_lodge_admin};
  json.event_statuses.each(function(es) {
    event_participant_status[es.event_id] = es.status;
  });
  
  //document.title = 'Tsar Gustaf: ' + me.display_name;
  $('my_residence_link').href = 'http://' + me.name + '.tsargustaf.com';
  $('lodge_display_name').update(lodge.display_name);
  $('lodge_symbol').src = '/images/' + lodge.id + '.png';
  sound_enabled = json.sound_enabled == 1;
  $('sound_enabled').checked = sound_enabled;
  
  timezones.init('Europe/Stockholm');   // to be read from the json response
  timezones.load_json();
  
  render_notifications(parse_xml(json.unread_notifications));
  
  //if(has_flash()) {
    initialize_swfupload(json.session_data);
  //}
  
  // Render list of all lodges in "Visit lodge" popup
  html = '';
  json.lodges.each(function(l) {
    html += '<div style="float: left; margin: 10px;">';
    html += ' <img src="/images/' + l.id + '.png" /><br />';
    html += ' <a href="#" onclick="visit_lodge(' + l.id + ', \'' + l.name + '\', \'' + l.display_name + '\'); return false;">' + l.display_name + '</a>';
    html += '</div>';
  });
  $('lodge_list').update(html);
  
  initialize_xmpp();
}

function visit_lodge(lodge_id, lodge_name, lodge_display_name) {
  // leave current lodge
	var cg_pres = new JSJaCPresence();
	cg_pres.setTo(lodge.name + '@conference.tsargustaf.com');
	cg_pres.setType('unavailable');
	cg_pres.setStatus('Besöker annan loge');
	cg_pres.setPriority(5);
	con.send(cg_pres);
	
	$('cg_messages').update('');
	
	// clean up sidebar
	$$('.presence_item').each(function(pi) {
	  if(pi.hasClassName('contact')) {
	    pi.removeClassName('in_lodge');
	  } else {
	    pi.remove();
	  }
	});
	
	// Reset local caches that depend on the lodge
	//local_cache.unset('events.js');
	local_cache['events.js'] = null;
	local_cache['info.js'] = null;
	// TODO: for all ?page=, maybe other stuff as well
	// FIXME: use Hash's set & unset methods... otherwise just using object itself. stupid.
	// can't just empty it since it also holds e.g. 'lounge'
  
  // enter new chatroom
  var cg_pres = new JSJaCPresence();
	cg_pres.setTo(lodge_name + '@conference.tsargustaf.com/' + me.display_name);
	con.send(cg_pres);
	
	lodge.id = lodge_id;
	lodge.name = lodge_name;
	lodge.display_name = lodge_display_name;
	lodge.is_admin = false;   // FIXME: this should be checked with the database
	
	$('lodge_display_name').update(lodge_display_name);
  $('lodge_symbol').src = '/images/' + lodge_id + '.png';
  
  $('visit_lodge').hide();
}

function initialize_xmpp() {
	// Start the XMPP client, get the roster, join court gossip chatroom etc.
	oDbg = new JSJaCConsoleLogger(2);	// debugging function
	
	con = new JSJaCHttpBindingConnection({'httpbase': "/http-bind/", 'timerval': 2000, 'oDbg': oDbg});
	setup_con(con);
	
  connect_now();
}

function initialize_swfupload(session_data) {
  var settings = {
    flash_url: "/swfupload_f9.swf",
    upload_url: "/main/upload",       // Relative to the SWF file (this changes dynamically later)
    post_params: {"session_data": session_data},    // this must be updated when session data important to the upload changes (does it ever?)
    file_size_limit: "20 MB",
    file_types: "*.jpeg;*.jpg;*.png;*.gif;*.bmp",
    file_types_description: "Bilder",
    file_upload_limit: 100,
    file_queue_limit: 0,
    custom_settings: {
      progressTarget: "fsUploadProgress",
      cancelButtonId: "btnCancel"
    },
    debug: false,
    
    // The event handler functions defined in handlers.js
    file_queued_handler: fileQueued,
    file_queue_error_handler: fileQueueError,
    file_dialog_complete_handler: fileDialogComplete,
    upload_start_handler: uploadStart,
    upload_progress_handler: uploadProgress,
    upload_error_handler: uploadError,
    upload_success_handler: uploadSuccess,
    upload_complete_handler: uploadComplete,
    queue_complete_handler: queueComplete	// Queue plugin event
  };
  
  swfu = new SWFUpload(settings);
}

// really simple history stuff
var yourListener = function(newLocation, historyData) {
  navigate_to(newLocation, true);
}

// Connect to the XMPP server
function connect_now() {
	$('input').update('Ansluter...');
	
	args = {};
	args.domain = "tsargustaf.com";
	args.username = me.name;
	args.resource = 'TsarGustaf Web ' + (new Date()).getTime();
	args.pass = encode_utf8(me.password);   // without the utf8-encode we get authorization error from the xmpp server
	con.connect(args);  
}

function new_name_keyup(e) {
  var elem = e.element();
  elem.value = elem.value.toLowerCase();
  if(/[^a-z0-9\-_]/.test(elem.value))
    $('new_name_warning').update('Använd bara a-z, 0-9, - och _ (inga suspekta tecken)');
  else
    $('new_name_warning').update('');
}

// Send a chat message
function send_muc(form) {
	if (form.msg.value == '')
		return false;
	
  try {
    var msg = new JSJaCMessage();
		msg.setType('groupchat');
    msg.setTo(lodge.name + '@conference.tsargustaf.com');
    msg.setBody(form.msg.value);
    con.send(msg);
		
    form.msg.value = '';
		form.msg.focus();
		
		return false;
	} catch (e) {
		alert("Error: " + e.message);
		return false;
	}
}

function render_present_me(node, resource, presence_type, status) {
  var html = '';
  html += '<div class="presence_item presence_item_me" id="cg_presence_' + node + '">';
  html +=   '<img src="/images/portraits/' + node + '_small.png?' + portrait_cache_string + '" class="' + node + '_portrait presence_portrait" />';
  html += 	'<div class="chatter_info">';
  html += 		'<div>' + resource + '</div>';
  // TODO: add switcher stuff here
  //html += 		'<span class="chatter_presence" id="chatter_' + node + '_presence">';
  //html +=       presence_type;
  //html += 		'</span>';
  html += 		'<select class="chatter_presence_select" id="chatter_presence_select" onchange="$(\'chatter_' + node + '_status\').hide(); $(\'change_my_status_link\').hide(); $(\'change_my_status\').show(); $(\'new_status\').activate(); return false;">';
  html += 			'<option value="available">här</option>';
  html += 			'<option value="away">borta</option>';
  html += 		'</select>';
  html += 		'<span class="chatter_status" id="chatter_' + node + '_status">';
  html += 			'' + status + '';
  html += 		'</span>';
  html += 			'<span id="change_my_status_link">';
  html += 				'<a href="#" onclick="$(\'chatter_' + node + '_status\').hide(); $(\'change_my_status_link\').hide(); $(\'change_my_status\').show(); $(\'new_status\').activate(); return false;">(skriv ett statusmeddelande)</a>';
  html += 			'</span>';
  html += 			'<div id="change_my_status" style="display: none">';
  html += 				'<form action="/change_status" method="post" onsubmit="change_presence(); return false;">';
  html += 				  '<fieldset>';
  html += 					  '<input type="text" name="new_status" value="' + status + '" size="11" id="new_status" />';
  html += 					  '<input type="submit" value="Byt!" id="status_submit" />';
  html += 				  '</fieldset>';
  html += 				'</form>';
  html += 			'</div>';
  html += 	'</div>';
  html += '</div>';
  
  return html;
}

function render_present_member(node, resource, presence_type, status) {
  var html = '';
  html += '<div class="presence_item" id="cg_presence_' + node + '">';
  html +=   '<img src="/images/portraits/' + node + '_tiny.png?' + portrait_cache_string + '" class="' + node + '_portrait presence_portrait" />';
  html += 	'<div class="chatter_info">';
  html += 		'<div class="presence_links"><!--img src="/images/comment.png" /--> <a href="http://' + node + '.tsargustaf.com" target="_blank"><img src="/images/house.png" alt="Hus" title="Besök residens" /></a></div>';
  html += 		'<div>' + resource + '</div>';
  html += 		'<span class="chatter_presence" id="chatter_' + node + '_presence">';
  html +=       presence_type;
  html += 		'</span>';
  html += 		'<span class="chatter_status" id="chatter_' + node + '_status">';
  html += 			status;
  html += 		'</span>';
  html += 	'</div>';
  html +=   '<div style="clear: both;"></div>';
  html += '</div>';
  
  return html;
}

function reset_input_div() {
  html = '<form action="#" id="cg_input" name="muc_message" onsubmit="return send_muc(this);">';
	html +=   '<fieldset>';
  html += 		'<div>';
  html += 			'<input type="text" name="msg" id="msg_area" size="30" />';
  html += 			'<input type="submit" value="Säg det!" />';
  html += 		'</div>';
  html += 	'</fieldset>';
	html += '</form>';
	
  $('input').update(html);
}

function change_presence() {
  $('status_submit').focus();
  $('status_submit').disabled = true;
  
  // send pres to roster
	var my_pres = new JSJaCPresence();
	my_pres.setPriority(5);
	my_pres.setShow($('chatter_presence_select').value);
	if($('new_status').value != '')
	  my_pres.setStatus($('new_status').value);
	con.send(my_pres);
	
	// send to court gossip room
	my_pres = new JSJaCPresence();
	my_pres.setTo(lodge.name + '@conference.tsargustaf.com/' + me.display_name)
	my_pres.setPriority(5);
	my_pres.setShow($('chatter_presence_select').value);
	if($('new_status').value != '')
	  my_pres.setStatus($('new_status').value);
	con.send(my_pres);
  
	//$('chatter_' + me.name + '_status').update($('new_status').value);
	if($('new_status').value == '')
	  $$('#change_my_status_link a').first().update('(skriv ett statusmeddelande!)');
	else
	  $$('#change_my_status_link a').first().update('(byt!)');
	
	$('change_my_status').hide();
	$('chatter_' + me.name + '_status').show();
	$('change_my_status_link').show();
	$('status_submit').disabled = false;
	$('msg_area').focus();
}

function change_password() {
  $('change_password_ok').hide();
  $('change_password_error').hide();
  new Ajax.Request('/change_password',
    { asynchronous: true,
      evalScripts: false,
      parameters: Form.serialize($('change_password_form')),
      onFailure: function() {
        new Effect.Appear('change_password_error');
        setTimeout("$('change_password_error').hide();", 5000);
      },
      onSuccess: function() {
        new Effect.Appear('change_password_ok');
        me.password = $('new_password').value;
        $('old_password').value = '';
        $('new_password').value = '';
        setTimeout("$('change_password_ok').hide();", 5000);
      }
    }
  );
}

function member_portrait_updated(member_name) {
  // this is global and essentially makes all portrait images reload next time they're displayed
  portrait_cache_string = (new Date()).getTime();
  
  // reload all images with this particular portrait right away
  $$('img.' + member_name + '_portrait').each (function(img) {
    img.src = img.src.split('?', 2)[0] + '?' + portrait_cache_string;
  });
}

function show_lounge() {
  navigate_to('lounge');
}

function show_events() {
  // load events
  // make sure we have "back to all events" or similar for all event pages before doing this
  //if($('events').innerHTML == '') {
    navigate_to('events.js');
  // } else {
  //   $$('.tab_content').invoke('hide');
  //   $('events').show();
  // }
  $('events_button').blur();
}

function show_info() {
  navigate_to('info.js');
  $('info_button').blur();
}

// Callback when we are connected to the XMPP server
function handle_connected() {
  $('cg_messages').update('');
	$('cg_presence').update('');
	reset_input_div();
	$('msg_area').focus();

	// request roster
	var roster_iq = new JSJaCIQ();
	roster_iq.setIQ('', 'get', me.id + 'r1');
	roster_iq.setQuery('jabber:iq:roster');
	con.send(roster_iq);
	
	// rest of procedure in handle_iq
	// ie sending initial presence & joining the lodge chat
}

// Handle being disconnected from XMPP server
function handle_disconnected() {
	// reconnect again ffs. TODO: make this into something more like gmails exponential tcp-style resending thingy. mkay?
	$('input').update('Du har blivit frånkopplad. <input type="button" value="Återanslut" onclick="connect_now(); return false;" />');
}

// Handle when the status changes
function handle_status_changed(status) {
  oDbg.log("status changed: " + status);
}

// Handle received presence packet
function handle_presence(aJSJaCPacket) {
  var type_translations = {'available': 'är tillgänglig', 'chat': 'är tillgänlig för chatt', 'away': 'är borta', 'xa': 'är ej tillgänglig', 'dnd': 'vill ej bli störd', 'unavailable': 'lämnade chatten'};
  
  var presence_type = ''; // This is used for the red text in the presence list, hence ignore if presence is available
  if(aJSJaCPacket.getShow() || aJSJaCPacket.getType() || 'available' != 'available') {
    var presence_list_type_translations = {'chat': 'tillgänlig för chatt', 'away': 'borta', 'xa': 'ej tillgänglig', 'dnd': 'vill ej bli störd', 'unavailable': 'frånkopplad'};
    presence_type = presence_list_type_translations[(aJSJaCPacket.getShow() || aJSJaCPacket.getType() || 'available')];
  }
  
	var from_jid = aJSJaCPacket.getFromJID();
	if (from_jid.getNode() == me.name || from_jid.getNode() == 'bot') {
	  // Ignore presence from the bot
		return;
	}
	else if(aJSJaCPacket.getChild('x', 'vcard-temp:x:update')) {
	  // if this is a presence for announcing a new photo, reload photo and ignore presence
    member_portrait_updated(cg_display_name_to_node.get(from_jid.getResource()));
  }
  else if(aJSJaCPacket.getType() == 'error') {
    // error packet
    // if it's a nickname collision for the lodge muc, generate a new nick
    if(aJSJaCPacket.getChild('error').getAttribute('code') == '409') {
      //alert('WE\'VE GOT A 4-0-9 DOWN ON MUC ROOM LANE, SUSPECTED NICKNAME COLLISION, DISPATCH ALL UNITS');
      //var new_nick = prompt('Ditt namn är tyvärr redan upptaget i logechatten, var god välj ett annat', me.display_name);
      var new_nick = me.display_name + '_' + Math.floor(Math.random() * 10000);
    	var cg_pres = new JSJaCPresence();
    	cg_pres.setTo(lodge.name + '@conference.tsargustaf.com/' + new_nick);
    	con.send(cg_pres);
    }
  }
  else {
    var user_node = null;
    
    var status_str = '';
		if(aJSJaCPacket.getStatus() != '') {
		  status_str = ' (' + aJSJaCPacket.getStatus() + ')';
		}
    
    if(from_jid.getDomain() == 'conference.tsargustaf.com') {
  		if(from_jid.getNode() == lodge.name) {
        user_node = cg_display_name_to_node.get(from_jid.getResource());
        
  		  // write new status to chat log
  			// but don't show if it's this member's initial login, then write "logged in" (further down)
  			if($('cg_presence_' + user_node) != null && (!$('cg_presence_' + user_node).hasClassName('contact') || $('cg_presence_' + user_node).hasClassName('in_lodge'))) {
    			new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short('now', false, true) + '</div></div><div class="cg_body">' + from_jid.getResource() + ' ' + type_translations[(aJSJaCPacket.getShow() || aJSJaCPacket.getType() || 'available')] + status_str + '</div></div>');
    			$('cg_messages').lastChild.scrollIntoView();
    			if(dhtmlHistory.isSafari) $('container').scrollIntoView();
  			}
        
  			// someone became available -- add them to presence list & save node-resource mapping
  			if (!aJSJaCPacket.getType()) {
  			  var real_jid = null;
  			  var item = aJSJaCPacket.getChild('item', '*');
  			  if(item && item.getAttribute('jid')) {
   				  real_jid = new JSJaCJID(item.getAttribute('jid'));
  				  cg_display_name_to_node.set(from_jid.getResource(), real_jid.getNode());
  				  user_node = real_jid.getNode();
  			  }
          
  				if($('cg_presence_' + user_node) == null) {
  				  // special case for login
      			// if cg_messages is empty it's the initial presence, don't show
  				  if(!$('cg_messages').innerHTML.blank()) {
    				  new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short('now', false, true) + '</div></div><div class="cg_body">' + from_jid.getResource() + ' loggade in' + status_str + '</div></div>');
        			$('cg_messages').lastChild.scrollIntoView();
        			if(dhtmlHistory.isSafari) $('container').scrollIntoView();
  				  }
            
  				  if(real_jid != null) {
    				  // put this one in the right place in the online list
    				  if(user_node == me.name) {
    				    $('cg_presence_me').insert(render_present_me(user_node, from_jid.getResource(), presence_type, aJSJaCPacket.getStatus()));
    				  } else if(contacts.indexOf(user_node) > -1) {
    				    // if we get the contact's MUC presence before the regular presence
    				    $('cg_presence_contacts').insert(render_present_member(user_node, from_jid.getResource(), presence_type, aJSJaCPacket.getStatus()));
            	  $('cg_presence_' + user_node).addClassName('contact').addClassName('in_lodge');
    				  } else if(contacts.indexOf(user_node) == -1) {
    				    $('cg_presence').insert(render_present_member(user_node, from_jid.getResource(), presence_type, aJSJaCPacket.getStatus()));
    				  }
  				  } else {
  				    $('cg_presence').insert('<div class="presence_item">' + from_jid.getResource() + '</div>');
  			    }
  				} else {
  				  if(contacts.indexOf(user_node) > -1 && !$('cg_presence_' + user_node).hasClassName('in_lodge')) {
  				    // this is a contact - that's why he already existed, from a regular presence
  				    $('cg_presence_' + user_node).addClassName('in_lodge');
              
        			// if cg_messages is empty it's the initial presence, don't show
    				  if(!$('cg_messages').innerHTML.blank()) {
      				  new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short('now', false, true) + '</div></div><div class="cg_body">' + from_jid.getResource() + ' loggade in' + status_str + '</div></div>');
          			$('cg_messages').lastChild.scrollIntoView();
          			if(dhtmlHistory.isSafari) $('container').scrollIntoView();
    				  }
  				  }
  				}
  			}
        
  			// someone signed off, remove them from list
  			else if(aJSJaCPacket.getType() == 'unavailable') {
  				if($('cg_presence_' + user_node)) {
  				  if(contacts.indexOf(user_node) > -1) {
    			    $('cg_presence_' + user_node).removeClassName('in_lodge');
    			  } else {
    					$('cg_presence_' + user_node).remove();
    			  }
  					cg_display_name_to_node.unset(from_jid.getResource());
  				}
  			}
  		}
  	} else {
  	  // regular presence, not from MUC
  	  user_node = from_jid.getNode();
      if (!aJSJaCPacket.getType()) {
    	  if($('cg_presence_' + user_node) == null) {
    	    // in case we get this presence before the MUC presence, or if the contact is not in the MUC
      	  $('cg_presence_contacts').insert(render_present_member(user_node, contact_node_to_display_name.get(from_jid.getNode()), presence_type, aJSJaCPacket.getStatus()));
      	  $('cg_presence_' + user_node).addClassName('contact');
    	  }
    	} else if(aJSJaCPacket.getType() == 'unavailable') {
		    // This often comes before the presence reveiced via the muc room -- so we have to show the "logged out" message here too
		    if($('cg_presence_' + user_node) && $('cg_presence_' + user_node).hasClassName('in_lodge')) {
    			new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short('now', false, true) + '</div></div><div class="cg_body">' + contact_node_to_display_name.get(from_jid.getNode()) + ' ' + type_translations[(aJSJaCPacket.getShow() || aJSJaCPacket.getType() || 'available')] + status_str + '</div></div>');
    			$('cg_messages').lastChild.scrollIntoView();
    			if(dhtmlHistory.isSafari) $('container').scrollIntoView();
		    }
    	  $('cg_presence_' + user_node).remove();
    	}
  	}
  	// this happens in both cases (from MUC or not)
  	
		// if status changed, change the presence list
		if($('cg_presence_' + user_node) != null) {
		  if(user_node != me.name) $('chatter_' + user_node + '_presence').update(presence_type);
		  $('chatter_' + user_node + '_status').update(aJSJaCPacket.getStatus());
		}
  }
}

// Handle message packet
function handle_message(aJSJaCPacket) {
	if(aJSJaCPacket.getType() == 'groupchat') {
		var muc_jid = aJSJaCPacket.getFromJID();
		if(muc_jid.getNode() == lodge.name) {
		  var status = aJSJaCPacket.getChild('status', 'http://jabber.org/protocol/muc#user');
		  if(status && status.getAttribute('code') == '100') {
		    // info message "room non anonymous"
		    return;
	    }
	    
	    var time = 'now';
	    var x_delay = aJSJaCPacket.getChild('x', 'jabber:x:delay');
		  if(x_delay) {
		    var stamp = x_delay.getAttribute('stamp');
		    time = stamp.substring(0, 4) + '-' + stamp.substring(4,6) + '-' + stamp.substring(6);
		    var old_msg = true;
	    } else {
		    var old_msg = false;
	    }
	    
	    var msg_body = aJSJaCPacket.getBody().htmlEnc();
	    
	    // if there are any images (jpg, jpeg, png, gif) show it below the message (small)
	    var images_body = '';
		  var pics = msg_body.match(/((http:\/\/)|([^\/]|^)(www\.))([-\w]+(\.[-\w]+)*(:\d+)?(\/([~\w\+\.@%-]+)?)*(\.(jpg|jpeg|png|gif))(\?[\w\+@%&=.;-]+)?)/ig);
		  if(pics != null) {
		    pics.each(function(pic) {
		      if(pic == msg_body) {
		        msg_body = '';  // If the link to the image is the entire message content, replace it instead
		      }
		      images_body += '<div style="float: left; margin: 5px;"><a href="' + pic + '" target="_blank"><img src="' + pic + '" alt="Bild refererad i meddelandet" style="max-height: 75px;" /></a></div>';
		    });
		  }
	    
	    // auto-link URLs with http:// or no protocol specified
	    msg_body = msg_body.replace(/((http:\/\/)|([^\/]|^)(www\.))([-\w]+(\.[-\w]+)*(:\d+)?(\/(([~\w\+@%-]|([,.;:][^\s$]))+)?)*(\?[\w\+@%&=.;-]+)?(\#[\w\-]*)?)/ig, "$3<a href='http://$4$5' target='_blank'>$2$4$5</a>");
	    // auto-link https-urls (can be extended to include any protocol if necessary)
	    msg_body = msg_body.replace(/(https:\/\/)([-\w]+(\.[-\w]+)*(:\d+)?(\/(([~\w\+@%-]|([,.;:][^\s$]))+)?)*(\?[\w\+@%&=.;-]+)?(\#[\w\-]*)?)/ig, "<a href='$1$2' target='_blank'>$1$2</a>");
	    
	    msg_body += images_body;
		  
	    if(old_msg) {
	      // if it's old, we dont have the resource -> jid node connection. don't display avatar and make message gray
	      if(msg_body.startsWith('/me ')) {
	        // me-command
    			new Insertion.Bottom('cg_messages', '<div class="cg_message old_cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short(time, false, true) + '</div></div><div class="cg_body">' + muc_jid.getResource() + ' ' + msg_body.substr(4) + '</div></div>');
	      } else {
	        // regular message
    			new Insertion.Bottom('cg_messages', '<div class="cg_message old_cg_message"><div class="cg_name"><div class="cg_display_name">' + muc_jid.getResource() + '</div><div class="cg_timestamp">' + time_nice_short(time, false, true) + '</div></div><div class="cg_body">' + msg_body + '</div></div>');	        
	      }
	    } else {
	      if(msg_body.startsWith('/me ')) {
    			new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_timestamp">' + time_nice_short(time, false, true) + '</div></div><div class="cg_body">' + muc_jid.getResource() + ' ' + msg_body.substr(4) + '</div></div>');
	      } else {
  			  new Insertion.Bottom('cg_messages', '<div class="cg_message"><div class="cg_name"><div class="cg_avatar"><img src="/images/portraits/' + cg_display_name_to_node.get(muc_jid.getResource()) + '_tiny.png?' + portrait_cache_string + '" class="' + cg_display_name_to_node.get(muc_jid.getResource()) + '_portrait" /></div><div class="' + cg_display_name_to_node.get(muc_jid.getResource()) + '_display_name cg_display_name">' + muc_jid.getResource() + '</div><div class="cg_timestamp">' + time_nice_short(time, false, true) + '</div></div><div class="cg_body">' + msg_body + '</div></div>');
  			}
      }
			$('cg_messages').lastChild.scrollIntoView();
			if(dhtmlHistory.isSafari)
			  $('container').scrollIntoView();   // safari's scrollIntoView implementation is buggy
			
			if(!old_msg) {
			  // sometimes we switch app while waiting to connect, then we don't want all old messages to pop up
  			if(window.fluid != null && !is_active_window) {
  			  var unread_msgs = parseInt(window.fluid.dockBadge) || 0;
  			  window.fluid.dockBadge = unread_msgs + 1;
  			  window.fluid.showGrowlNotification({
              title: muc_jid.getResource() + " skrev",
              description: msg_body,
              priority: 1,
              sticky: false,
              identifier: aJSJaCPacket.getID(),
              onclick: function() {window.focus()},
              icon: 'http://tsargustaf.com/images/portraits/' + cg_display_name_to_node.get(muc_jid.getResource()) + '_tiny.png'
          })
  			}
  			if(sound_enabled && !is_active_window) {
  			  Sound.play('/you_got_it.wav');
  			}			  
			}
		}
	}
	else if(aJSJaCPacket.getFromJID().getDomain() == 'pubsub.tsargustaf.com' || aJSJaCPacket.getFromJID().getNode() == 'bot') {
	  var items = aJSJaCPacket.getChild('items', '*');
	  if(items) {
	    render_notifications(items);
	  }
	  //alert(aJSJaCPacket.xml());
	}
}

function parse_xml(xml_string) {
  // notifications is an xml string consisting of one or more notification elements
  var xml_doc;
  if(dhtmlHistory.isIE) {
    try {
      // Internet Explorer
      xml_doc = new ActiveXObject("Microsoft.XMLDOM");
      xml_doc.async = "false";
      xml_doc.loadXML(xml_string);
    } catch(e) {
      alert(e.message);
    }
  } else {
    try {
      // Firefox, Mozilla, Opera, etc.
      var parser = new DOMParser();
      xml_doc = parser.parseFromString(xml_string, "text/xml");
    } catch(e) {
      alert(e.message);
    }
  }
  return xml_doc;
}

function render_notifications(notifications) {
  // notifications is an XML document containing one or more notifications
  ns = notifications.getElementsByTagName('notification');
  $A(ns).each(function(n) {
    var sender = n.getElementsByTagName('sender')[0];
    if(sender.getAttribute('id') != me.id) {
      if(n.getAttribute('type') == 'post') {
        var this_post_id = 'post_' + n.getAttribute('id');
        var html = ['<li id="' + this_post_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' <b>' + sender.getAttribute('display_name') + '</b> skrev <b>dagboksanteckning</b>: &rdquo;' + n.getElementsByTagName('title')[0].childNodes[0].nodeValue + '&rdquo;<br />',
                      '<a href="' + n.getElementsByTagName('permalink')[0].childNodes[0].nodeValue + '" onclick="remove_unread_notification(\'' + this_post_id + '\');" target="_blank">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_post_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_post_id, n.xml);

        // <li style="margin: 3px; border: 1px black solid;">
        //   19/5 21.03 jopsy dagbok: &rdquo;Bland trasiga syltburkar&rdquo;<br />
        //   Visa - Visa nytt fönster - Markera som läst
        // </li>
      } else if(n.getAttribute('type') == 'pictures') {
        var this_item_id = 'pictures_' + n.getAttribute('id');
        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' <b>' + sender.getAttribute('display_name') + '</b> laddade upp <b>bilder</b><br />',
                      '<a href="' + n.getElementsByTagName('permalink')[0].childNodes[0].nodeValue + '" onclick="remove_unread_notification(\'' + this_item_id + '\');" target="_blank">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];
        
        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);
      } else if(n.getAttribute('type') == 'post_comment') {
        var this_item_id = 'post_comment_' + n.getAttribute('id');
        var post_author = n.getElementsByTagName('post_author')[0];
        var whos = '';
        if(post_author.getAttribute('name') == me.name) {
          whos = 'din'
        } else {
          var use_s = true;
          (['s', 'x', 'z', 'sh', 'ch']).each(function(ending) {
            if(post_author.getAttribute('display_name').endsWith(ending))
              use_s = false;
          });
          if(use_s)
            whos = post_author.getAttribute('display_name') + 's';
          else
            whos = post_author.getAttribute('display_name');
        }

        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' ' + sender.getAttribute('display_name') + ' <b>kommenterade</b> på ' + whos + ' dagboksanteckning <b>&rdquo;' + n.getElementsByTagName('post_title')[0].childNodes[0].nodeValue + '&rdquo;</b><br />',
                      '<a href="' + n.getElementsByTagName('permalink')[0].childNodes[0].nodeValue + '" onclick="remove_unread_notification(\'' + this_item_id + '\');" target="_blank">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);      
      } else if(n.getAttribute('type') == 'picture_comment') {
        var this_item_id = 'picture_comment_' + n.getAttribute('id');
        var picture_author = n.getElementsByTagName('picture_author')[0];
        var whos = '';
        if(picture_author.getAttribute('name') == me.name) {
          whos = 'din'
        } else {
          var use_s = true;
          (['s', 'x', 'z', 'sh', 'ch']).each(function(ending) {
            if(picture_author.getAttribute('display_name').endsWith(ending))
              use_s = false;
          });
          if(use_s)
            whos = picture_author.getAttribute('display_name') + 's';
          else
            whos = picture_author.getAttribute('display_name');
        }

        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' ' + sender.getAttribute('display_name') + ' <b>kommenterade</b> på ' + whos + ' bild <b>&rdquo;' + n.getElementsByTagName('picture_description')[0].childNodes[0].nodeValue + '&rdquo;</b><br />',
                      '<a href="' + n.getElementsByTagName('permalink')[0].childNodes[0].nodeValue + '" onclick="remove_unread_notification(\'' + this_item_id + '\');" target="_blank">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);      
      } else if(n.getAttribute('type') == 'event') {
        var this_item_id = 'event_' + n.getAttribute('id');
        var lodge = n.getElementsByTagName('lodge')[0];
        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' ' + sender.getAttribute('display_name') + ' planerar en <b>ny träff</b> för ' + lodge.getAttribute('display_name') + ': <b>&rdquo;' + n.getElementsByTagName('title')[0].childNodes[0].nodeValue + '&rdquo;</b><br />',
                      '<a href="#" onclick="navigate_to(\'events/' + n.getAttribute('id') + '.js\'); remove_unread_notification(\'' + this_item_id + '\'); return false;">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);
      } else if(n.getAttribute('type') == 'event_comment') {
        var this_item_id = 'event_comment_' + n.getAttribute('id');
        var lodge = n.getElementsByTagName('lodge')[0];
        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' ' + sender.getAttribute('display_name') + ' <b>kommenterade</b> på träffen <b>' + n.getElementsByTagName('event_title')[0].childNodes[0].nodeValue + '</b><br />',
                      '<a href="#" onclick="navigate_to(\'events/' + n.getElementsByTagName('event_id')[0].childNodes[0].nodeValue + '.js\'); remove_unread_notification(\'' + this_item_id + '\'); return false;">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);
      } else if(n.getAttribute('type') == 'event_pictures') {
        var this_item_id = 'event_pictures_' + n.getAttribute('id');
        var lodge = n.getElementsByTagName('lodge')[0];
        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' ' + sender.getAttribute('display_name') + ' laddade upp <b>bilder</b> till träffen <b>' + n.getElementsByTagName('event_title')[0].childNodes[0].nodeValue + '</b><br />',
                      '<a href="#" onclick="navigate_to(\'events/' + n.getElementsByTagName('event_id')[0].childNodes[0].nodeValue + '.js\'); remove_unread_notification(\'' + this_item_id + '\'); return false;">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);
      } else if(n.getAttribute('type') == 'external_feed_item') {
        var this_item_id = 'external_feed_item_' + n.getAttribute('id');
        var html = ['<li id="' + this_item_id + '">',
                      time_nice_short(n.getAttribute('created_at'), false, true) + ' <b>' + sender.getAttribute('display_name') + '</b> postade ett <b>inlägg</b>: &rdquo;' + n.getElementsByTagName('title')[0].childNodes[0].nodeValue + '&rdquo;<br />',
                      '<a href="' + n.getElementsByTagName('permalink')[0].childNodes[0].nodeValue + '" onclick="remove_unread_notification(\'' + this_item_id + '\');" target="_blank">Visa</a> ',
                      '<a href="#" onclick="remove_unread_notification(\'' + this_item_id + '\'); return false;">Markera som läst</a>',
                    '</li>'];

        $('notification_list').insert({top: html.join('')});
        unread_notifications.set(this_item_id, n.xml);
      }      
    }
  });
}

function remove_unread_notification(item_id) {
  // id is e.g. "post_12345"
  $(item_id).remove();
  unread_notifications.unset(item_id);
}

// Handle a received IQ packet
function handle_iq(aIQ) {
	if(aIQ.getID() == me.id + 'r1' && aIQ.getQueryXMLNS() == 'jabber:iq:roster') {
		// roster response
		contacts.clear();
		var items = aIQ.getQuery().childNodes;
		for(var i = 0; i < items.length; i++) {
  	  if(items[i].getAttribute('subscription') == 'none') {
  	    if(items[i].getAttribute('ask') == 'subscribe' && items[i].getAttribute('name') != null) {  // why is this != null needed? sometimes we get duplicates, one with a name and one with null
  	      var jid = new JSJaCJID(items[i].getAttribute('jid'));
  	      //new Insertion.Top('conversation_contacts', '<div id="awaiting_request_' + jid.getNode() + '">Awaiting contact request from ' + items[i].getAttribute('name') + '</div>');
  	      // TODO: add as a notification?
        }
        // if not ask is set, show nothing
  	  } else if(items[i].getAttribute('subscription') == 'to' || items[i].getAttribute('subscription') == 'both') {
  			var jid = new JSJaCJID(items[i].getAttribute('jid'));
  			if(jid.getNode() != 'bot') {	// don't show the bot in the web client
  				//new Insertion.Bottom('conversation_contacts', render_contact(items[i], jid));
  				contacts.push(jid.getNode());
  				contact_node_to_display_name.set(jid.getNode(), items[i].getAttribute('name'));
  			}
  	  }
		}
		
  	// send initial presence
  	var my_pres = new JSJaCPresence();
  	my_pres.setPriority(5);	// otherwise it won't work with other instances of the web client
  	con.send(my_pres);
    
  	// join lodge muc room
  	var cg_pres = new JSJaCPresence();
  	cg_pres.setTo(lodge.name + '@conference.tsargustaf.com/' + me.display_name);
  	con.send(cg_pres);
	}
	else if(aIQ.getQueryXMLNS() == 'jabber:iq:roster') {
	  // a roster push, but not the initial roster response
		var item = aIQ.getChild('item');  // sometimes the item is not in the query element but after it.. unclear why, ejabberd bug?
		if(item == null)      // it seems like getChild() only gets immediate children, not descendants.. so do like this too
		  item = aIQ.getQuery().childNodes[0];  // should only contain one node right?
	  if(item.getAttribute('subscription') == 'none') {
	    if(item.getAttribute('ask') == 'subscribe' && item.getAttribute('name') != null) {  // why is this != null needed? sometimes we get duplicates, one with a name and one with null
  	    var jid = new JSJaCJID(item.getAttribute('jid'));
	      //new Insertion.Top('conversation_contacts', '<div id="awaiting_request_' + jid.getNode() + '">Awaiting contact request from ' + item.getAttribute('name') + '</div>');
	      // TODO: smth smart
      }
      // if not ask is set, show nothing
	  } else if(item.getAttribute('subscription') == 'to' || item.getAttribute('subscription') == 'both') {
	    // we just added/subscribed the mofo
	    var jid = new JSJaCJID(item.getAttribute('jid'));
	    //if($('roster_item_' + jid.getNode()) == null)
			//  new Insertion.Bottom('conversation_contacts', render_contact(item, jid));
			// TODO: if the fucker's online, put him in the "contacts online" list. remove from muc room list if he's there
	  }
	}
	else if(aIQ.getType() == 'result' && aIQ.getID() == me.id + 'for_upload_pic') {
	  // get vcard response for uploading new photo
	  update_vcards_photo(aIQ);
	}
	else if(aIQ.getID() == me.id + 'with_new_photo' && aIQ.getType() == 'result') {
	  // set vcard response
	  // at least for safari a short wait is needed, otherwise nothing is sent
	  //setTimeout("send_vcard_update();", 5000);
	  send_vcard_update();
	}
	else if(aIQ.getID().startsWith('ping')) {
	  // ping response for safari -- do nothing
	}
	else if(aIQ.getType() == 'result') {
	  // random response, just do nothing so we don't send the not implemented stanza.
	}
	else {
		con.send(aIQ.errorReply(ERR_FEATURE_NOT_IMPLEMENTED));
	}
}

// Handle error packet
function handle_error(e) {
  switch(e.getAttribute('code')) {
    case '503':
      $('cg_messages').update("Chatservern är för närvarande otillgänglig.");
      break;
    default:
    	$('cg_messages').update('<div>Ett fel inträffade:<br />' + ("Code: " + e.getAttribute('code') + "\nType: " + e.getAttribute('type') + "\nCondition: " + e.firstChild.nodeName).htmlEnc() + '</div>');
      break;
  }
  
  // TEMP: kind of temporary
  $('input').update('<input type="button" value="Återanslut" onclick="connect_now(); return false;" />')
  
	if (con.connected())
		con.disconnect();
}

function setup_con(con) {
    con.registerHandler('message', handle_message);
    con.registerHandler('presence', handle_presence);
    con.registerHandler('iq', handle_iq);
    con.registerHandler('onconnect', handle_connected);
    con.registerHandler('onerror', handle_error);
    con.registerHandler('status_changed', handle_status_changed);
    con.registerHandler('ondisconnect', handle_disconnected);
    
    // con.registerIQGet('query', NS_VERSION, handleIqVersion);
    // con.registerIQGet('query', NS_TIME, handleIqTime);
}

function uninitialize() {
	var my_pres = new JSJaCPresence();
	my_pres.setType('unavailable');
	my_pres.setStatus('Loggade ut');
	my_pres.setPriority(5);	// otherwise it won't work with other instances of the web client
	con.send(my_pres);
	
	con.disconnect();
	
	new Ajax.Request('/main/save_unread.js',
		{ asynchronous: false,    // works better than true (seems like prototype.js gets unloaded before the function starts processing)
			evalScripts: true,
			method: 'post',
			parameters: {'unread_notifications': '<items>' + unread_notifications.values().join('') + '</items>'}
		}
	);
}

function portrait_upload_ok(base64, sha1) {
  $('picture_wait').hide();
  $('picture_okay').show();
  photo_base64 = base64;
  photo_sha1 = sha1;
  upload_vcard_pic();
}

function portrait_upload_fail() {
  $('picture_wait').hide();
  $('picture_error').show();
}

// This updates the user's vcard-temp at the xmpp server and also sends out the vcard-temp:x:update
// which tells the clients to refresh this user's image (also used by the web client)
function upload_vcard_pic() {
  // request vcard, on handle iq when receiving it, change it and send the update
	var vcard_iq = new JSJaCIQ();
	vcard_iq.setIQ('', 'get', me.id + 'for_upload_pic');
	vcard_iq.appendNode('vCard', {xmlns: 'vcard-temp'});
	con.send(vcard_iq);
}

// This is called when we have received the vcard so we can update it with the new photo
function update_vcards_photo(aIQ) {
  var vcard_node = aIQ.getChild('vCard', 'vcard-temp');
  if(vcard_node == null) {
    // if the user has no vcard, add it
    aIQ.appendNode('vCard', {'xmlns': 'vcard-temp'});
    vcard_node = aIQ.getChild('vCard', 'vcard-temp');
  }
  var photo_node = aIQ.getChild('PHOTO');
  var new_photo_node = aIQ.buildNode('PHOTO', [aIQ.buildNode('TYPE', 'image/png'), aIQ.buildNode('BINVAL', photo_base64)]);
  if(photo_node) {
    vcard_node.replaceChild(new_photo_node, photo_node);
  } else {
    vcard_node.appendChild(new_photo_node);
  }
  
  var setIq = new JSJaCIQ();
  setIq.setIQ('', 'set', me.id + 'with_new_photo');
  // append the updated vCard node (this doesn't work in safari, must use importNode() to import it into the right xml document. but that doesn't work in ie. please do investigate)
  imported_node = xbImportNode(setIq.getDoc(), aIQ.getChild('vCard', 'vcard-temp'), true);
  setIq.getNode().appendChild(imported_node);
  con.send(setIq);
}

// This is called when we have confirmation from the server that the vcard is uploaded
function send_vcard_update() {
  // fucking thing sucks!! jsjac gets a problem when we send/receive this many stanzas rapidly, and
  // these two never get sent... weird really. "all slots busy, standby..." etc. what to do?
  // the only solution seems to be to wait for about 15 seconds. pretty long but one doesn't upload
  // new portraits all that often i guess. fugly implementation. every other message sent before 15 s
  // also get silently dropped by jsjac.
  setTimeout('send_vcard_update1();', 15000);
  setTimeout('send_vcard_update2();', 20000);
}

function send_vcard_update1() {
  var my_pres = new JSJaCPresence();
  my_pres.appendNode('x', {'xmlns': 'vcard-temp:x:update'}, [my_pres.buildNode('photo', photo_sha1)]);
  my_pres.setPriority(5);
  con.send(my_pres);
}

function send_vcard_update2() {
	// also send it to the MUC room (as per the spec, and to make the webapp work correctly)
  var my_pres2 = new JSJaCPresence();
  my_pres2.appendNode('x', {'xmlns': 'vcard-temp:x:update'}, [my_pres2.buildNode('photo', photo_sha1)]);
  my_pres2.setPriority(5);
  my_pres2.setTo(lodge.name + '@conference.tsargustaf.com/' + me.display_name);
  con.send(my_pres2);
}

function changed_sound_setting() {
  sound_enabled = $('sound_enabled').checked;
  
  // save the setting
  new Ajax.Request('/main/save_setting.js',
		{ asynchronous: true,
			evalScripts: true,
			method: 'post',
			parameters: {'sound_enabled': sound_enabled ? '1' : '0'}
		}
	);
}

// Helper stuff
function unescape_cookie(str) {
  return unescape(str.replace(/\+/g, ' '));  // unescape function doesn't handle + as space
}

// Formats a date for displaying
// date = string with UTC datetime in ISO8601 format or 'now'
function time_nice_short(date, include_today, short_month) {
	if(date == null || date == '') return '';
	if(include_today == null) include_today = true;   // if we should include "today, " if the time is today
	// takes a date string and returns a nicely formatted date string
	// todo: convert to member's timezone. localize.
	var ret = "";
  if(short_month)
	  var months = ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
	else
	  var months = ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'];
	
	// all javascript objects store time in UTC + local timezone. we're just going to use the UTC
	// time fields, but using them to store our local time, since we don't want to use the browser's
	// timezone setting but our own TG timezone setting.
	var d = new Date();   // d now contains current UTC time if we use getUTCHours() etc.
  
  // can pass 'now' to get current time
	if(date != 'now')
	  d.setISO8601(date);   // sets d's UTC time to date
	
	// we're now taking d's UTC time, converting it to local timezone and storing it back as UTC
	// this means we can't use the local time representation of the d object anymore, but must use
	// the functions that get UTC time (though it's the user's specified local time now)
	d = timezones.utc_to_local(d);
	
	var yesterday_end = timezones.utc_to_local(new Date());
	yesterday_end.setUTCDate(yesterday_end.getUTCDate() - 1);
	yesterday_end.setUTCHours(23); yesterday_end.setUTCMinutes(59); yesterday_end.setUTCSeconds(59);
	
	var yesterday_begin = timezones.utc_to_local(new Date());
	yesterday_begin.setUTCDate(yesterday_begin.getUTCDate() - 1);
	yesterday_begin.setUTCHours(0); yesterday_begin.setUTCMinutes(0); yesterday_begin.setUTCSeconds(0);
  
	var tomorrow_begin = timezones.utc_to_local(new Date());
	tomorrow_begin.setUTCDate(tomorrow_begin.getUTCDate() + 1);
	tomorrow_begin.setUTCHours(0); tomorrow_begin.setUTCMinutes(0); tomorrow_begin.setUTCSeconds(0);
	
	// Format the date
	if(d > yesterday_end && d < tomorrow_begin)
		ret = include_today ? 'i dag, ' : '';
	else if(d <= yesterday_end && d >= yesterday_begin)
		ret = 'i går, ';
	else if(d.getUTCFullYear() == timezones.utc_to_local(new Date()).getUTCFullYear())
		ret = d.getUTCDate() + ' ' + months[d.getUTCMonth()] + ', ';
	else
		ret = d.getUTCDate() + ' ' + months[d.getUTCMonth()] + ' ' + d.getUTCFullYear() + ', ';
	
	// Add the time
	ret = ret + d.getUTCHours() + ':' + (d.getUTCMinutes() < 10 ? '0' : '') + d.getUTCMinutes();
	
	return ret;
}

// Makes it possible to check if an object is an array or not.
// Kind of awkward to call it in two steps like this but all other methods bork in either safari or firefox...
// Will be fun to try it in IE.
function isArray(obj) {
	return obj.isArray;
}

Array.prototype.isArray = function() {
	return true;
}

// Date to/from W3C's subset of the ISO8601 date format
// taken from http://delete.me.uk/2005/03/iso8601.html
Date.prototype.setISO8601 = function (string) {
  if(string == '')
    return;
  
  var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
    "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
    "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
  var d = string.match(new RegExp(regexp));
  
  var offset = 0;
  var date = new Date(d[1], 0, 1);
  
  if (d[3]) { date.setMonth(d[3] - 1); }
  if (d[5]) { date.setDate(d[5]); }
  if (d[7]) { date.setHours(d[7]); }
  if (d[8]) { date.setMinutes(d[8]); }
  if (d[10]) { date.setSeconds(d[10]); }
  if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
  if (d[14]) {
    offset = (Number(d[16]) * 60) + Number(d[17]);
    offset *= ((d[15] == '-') ? 1 : -1);
  }
  
  offset -= date.getTimezoneOffset();
  time = (Number(date) + (offset * 60 * 1000));
  this.setTime(Number(time));
}

Date.prototype.toISO8601String = function(format, offset) {
  /* accepted values for the format [1-6]:
   1 Year:
     YYYY (eg 1997)
   2 Year and month:
     YYYY-MM (eg 1997-07)
   3 Complete date:
     YYYY-MM-DD (eg 1997-07-16)
   4 Complete date plus hours and minutes:
     YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
   5 Complete date plus hours, minutes and seconds:
     YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
   6 Complete date plus hours, minutes, seconds and a decimal
     fraction of a second
     YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
  */
  if (!format) { var format = 6; }
  if (!offset) {
    var offset = 'Z';
    var date = this;
  } else {
    var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
    var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
    offsetnum *= ((d[1] == '-') ? -1 : 1);
    var date = new Date(Number(Number(this) + (offsetnum * 60000)));
  }
  
  var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; }
  
  var str = "";
  str += date.getUTCFullYear();
  if (format > 1) { str += "-" + zeropad(date.getUTCMonth() + 1); }
  if (format > 2) { str += "-" + zeropad(date.getUTCDate()); }
  if (format > 3) {
    str += "T" + zeropad(date.getUTCHours()) +
           ":" + zeropad(date.getUTCMinutes());
  }
  if (format > 5) {
    var secs = Number(date.getUTCSeconds() + "." +
               ((date.getUTCMilliseconds() < 100) ? '0' : '') +
               zeropad(date.getUTCMilliseconds()));
    str += ":" + zeropad(secs);
  } else if (format > 4) { str += ":" + zeropad(date.getUTCSeconds()); }
  
  if (format > 3) { str += offset; }
  return str;
}

Date.prototype.toDatabaseString = function() {
  var date = this;
  var zeropad = function(num) { return ((num < 10) ? '0' : '') + num; }
  
  var str = '';
  str += date.getUTCFullYear();
  str += "-" + zeropad(date.getUTCMonth() + 1);
  str += "-" + zeropad(date.getUTCDate());
  str += " " + zeropad(date.getUTCHours()) +
         ":" + zeropad(date.getUTCMinutes()) +
         ":" + zeropad(date.getUTCSeconds());
  
  return str;
}

// These are taken from http://www.quirksmode.org/js/cookies.html
function createCookie(name, value, days) {
	if (days) {
		var date = new Date();
		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
		var expires = "; expires=" + date.toGMTString();
	}
	else var expires = "";
	document.cookie = name + "=" + value + expires + "; path=/";
}

function readCookie(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i = 0; i < ca.length; i++) {
		var 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;
}

function eraseCookie(name) {
	createCookie(name, "", -1);
}

xbImportNode = function(doc, node, allChildren) {
  if (!doc.ELEMENT_NODE) {
    doc.ELEMENT_NODE = 1;
    doc.ATTRIBUTE_NODE = 2;
    doc.TEXT_NODE = 3;
    doc.CDATA_SECTION_NODE = 4;
    doc.ENTITY_REFERENCE_NODE = 5;
    doc.ENTITY_NODE = 6;
    doc.PROCESSING_INSTRUCTION_NODE = 7;
    doc.COMMENT_NODE = 8;
    doc.DOCUMENT_NODE = 9;
    doc.DOCUMENT_TYPE_NODE = 10;
    doc.DOCUMENT_FRAGMENT_NODE = 11;
    doc.NOTATION_NODE = 12;
  }
  
  switch (node.nodeType) {
    case doc.ELEMENT_NODE:
      var newNode = doc.createElement(node.nodeName);
      // does the node have any attributes to add?
      if (node.attributes && node.attributes.length > 0)
        for (var i = 0, il = node.attributes.length; i < il; i++)
          newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i].nodeName));
      // are we going after children too, and does the node have any?
      if (allChildren && node.childNodes && node.childNodes.length > 0)
        for (var i = 0, il = node.childNodes.length; i < il; i++)
          newNode.appendChild(xbImportNode(doc, node.childNodes[i], allChildren));
      return newNode;
      break;
    case doc.TEXT_NODE:
    case doc.CDATA_SECTION_NODE:
    case doc.COMMENT_NODE:
      return doc.createTextNode(node.nodeValue);
      break;
  }
}  

// timezones object contain logic to convert between timezones using the olson database (zoneinfo)
// it's used by time_nice_short, since all dates to be displayed are run through that function
// it will also be used when converting dates given in local time to utc for sending to the server,
// e.g. when creating a new event.
// the only "public methods" that is to be called is utc_to_local() and local_to_utc().
var timezones = {
  init: function(timezone) {
    this.timezone = timezone;
    this.zones = {};
    this.rules = {};
    this.month_map = { 'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3,'may': 4, 'jun': 5,
      'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11 };
    this.dayMap = {'sun': 0,'mon' :1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6 };
  },
  load_json: function() {
    // ajax load json file
    // to generate a new json file:
    // ~/Desktop/mde-fleegix-js-javascript-toolkit-6e1bffac9c4b24caefd6818cfa01750e99c972fc/plugins/date anders$ java -cp ../../../rhino1_7R1/js.jar org.mozilla.javascript.tools.shell.Main preparse.js /Users/anders/tg_mini/zoneinfo "Europe/Stockholm" > stockholm.json
    var process_data = function(request) {
      var json = request.responseText.evalJSON();
      this.zones = json.zones;
      this.rules = json.rules;
    }
    
  	new Ajax.Request('/tz/' + this.timezone.gsub(/\//, '_').toLowerCase() + '.json',
  		{ asynchronous: false,    // false this
  			evalScripts: true,
  			method: 'get',
  			onComplete: process_data.bindAsEventListener(this)
  		}
  	);
  },
  utc_to_local: function(date) {
    // takes a date object and adjusts the time according to timezone. The timezone of the passed date
    // is ignored and should be ignored by all other code as well.
    var info = this.get_tz_info(date, this.timezone);
    //alert("Offset: " + info.tzOffset + " abbr: " + info.tzAbbr);
    var nd = new Date();
    nd.setTime(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds()) - info.tzOffset*60*1000);
    return nd;
  },
  local_to_utc: function(date) {
    var info = this.get_tz_info(date, this.timezone);
    //alert("Offset: " + info.tzOffset + " abbr: " + info.tzAbbr);
    var nd = new Date();
    nd.setTime(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds()) + info.tzOffset*60*1000);
    return nd;
  },
  get_tz_info: function(dt, tz) {
    var zone = this.get_zone(dt, tz);
    var off = this.getBasicOffset(zone);
    // See if the offset needs adjustment
    var rule = this.getRule(dt, zone[1]);
    if (rule) {
      off = this.getAdjustedOffset(off, rule);
    }
    var abbr = this.getAbbreviation(zone, rule);
    return { tzOffset: off, tzAbbr: abbr };
  },
  get_zone: function(dt, tz) {
    var t = tz;
    var zoneList = this.zones[t];
    // Follow links to get to an actual zone
    while(typeof zoneList == "string") {
      t = zoneList;
      zoneList = this.zones[t];
    }
    for(var i = 0; i < zoneList.length; i++) {
      var z = zoneList[i];
      if (!z[3]) { break; }
      var yea = parseInt(z[3]);
      var mon = 11;
      var dat = 31;
      if (z[4]) {
        mon = this.month_map[z[4].substr(0, 3).toLowerCase()];
        dat = parseInt(z[5]);
      }
      var t = z[6] ? z[6] : '23:59:59';
      t = this.parseTimeString(t);
      var d = Date.UTC(yea, mon, dat, t[1], t[2], t[3]);
      if(dt.getTime() < d) { break; }
    }
    if (i == zoneList.length) { alert('No Zone found for "' + timezone + '" on ' + dt); }
    return zoneList[i];
  },
  parseTimeString: function(str) {
    var pat = /(\d+)(?::0*(\d*))?(?::0*(\d*))?([wsugz])?$/;
    var hms = str.match(pat);
    hms[1] = parseInt(hms[1]);
    hms[2] = hms[2] ? parseInt(hms[2]) : 0;
    hms[3] = hms[3] ? parseInt(hms[3]) : 0;
    return hms;
  },
  getBasicOffset: function(z) {
    var off = this.parseTimeString(z[0]);
    var adj = z[0].indexOf('-') == 0 ? -1 : 1
    off = adj * (((off[1] * 60 + off[2]) *60 + off[3]) * 1000);
    return -off/60/1000;
  },
  getRule: function(dt, str) {
    var currRule = null;
    var year = dt.getUTCFullYear();
    var rules = this.rules[str];
    var ruleHits = [];
    var getMonthNumber = function (r) {
      return this.month_map[r[3].substr(0, 3).toLowerCase()];
    }.bind(this);
    var checkForHits = function (incr, rule, d, dt) {
      d.setUTCDate(d.getUTCDate() + incr);
      if (dt >= d) {
        ruleHits.push({ 'rule': rule, 'date': d });
      }
      // FIXME: Check against previous year if rule covers that period
      // These should always be fall rules from a previous year,
      // so the month number should be > 8 -- this is an ugly hack,
      // but seems to work consistently and I can't think of a better
      // way to cover all the goddamned edge cases for this -- see
      // Asia/Jerusalem for some particularly gnarly examples
      else if ((rule[0] < year) && (getMonthNumber(r) > 8)) {
        d.setUTCFullYear(d.getUTCFullYear()-1);
        if (dt >= d) {
          ruleHits.push({ 'rule': rule, 'date': d });
        }
      }
    }.bind(this);
    
    if (!rules) { return null; }
    for (var i = 0; i < rules.length; i++) {
      r = rules[i];
      // Only look at applicable rules -- throw out:
      // 1. Rules with a 'to' year earlier than the year
      // 2. Rules which are confined to the 'from' year, and are
      //  are earlier than the year
      // 3. Rules where the 'from' starts after
      if ((r[1] < year) ||
        (r[0] < year && r[1] == 'only') ||
        (r[0] > year)) {
        continue;
      };
      var mon = getMonthNumber(r);
      var day = r[4];
      
      // Not a specific date number -- have to parse to get date
      if (isNaN(day)) {
        if (day.substr(0, 4) == 'last') {
          var day = this.dayMap[day.substr(4,3).toLowerCase()];
          var t = this.parseTimeString(r[5]);
          // Last day of the month at the desired time of day
          var d = new Date(Date.UTC(dt.getUTCFullYear(), mon+1, 1, t[1]-24, t[2], t[3]));
          var dtDay = d.getUTCDay();
          // Set it to the final day of the correct weekday that month
          var incr = (day > dtDay) ? (day - dtDay - 7) : (day - dtDay);
          checkForHits(incr, r, d, dt);
        }
        else {
          day = this.dayMap[day.substr(0, 3).toLowerCase()];
          if (day != 'undefined') {
            if(r[4].substr(3, 2) == '>=') {
              var t = this.parseTimeString(r[5]);
              // The stated date of the month
              var d = new Date(Date.UTC(dt.getUTCFullYear(), mon,
                parseInt(r[4].substr(5)), t[1], t[2], t[3]));
              var dtDay = d.getUTCDay();
              // Set to the first correct weekday after the stated date
              var incr = (day < dtDay) ? (day - dtDay + 7) : (day - dtDay);
              checkForHits(incr, r, d, dt);
            }
            else if (day.substr(3, 2) == '<=') {
              var t = this.parseTimeString(r[5]);
              // The stated date of the month
              var d = new Date(Date.UTC(dt.getUTCFullYear(), mon,
                parseInt(r[4].substr(5)), t[1], t[2], t[3]));
              var dtDay = d.getUTCDay();
              // Set to first correct weekday before the stated date
              var incr = (day > dtDay) ? (day - dtDay - 7) : (day - dtDay);
              checkForHits(incr, r, d, dt);
            }
          }
        }
      }
      // Numeric date
      else {
        var t = this.parseTimeString(r[5]);
        var d = new Date(Date.UTC(dt.getUTCFullYear(), mon, day, t[1], t[2], t[3]));
        if (dt < d) {
          continue;
        }
        else {
          ruleHits.push({ 'rule': r, 'date': d });
        }
      }
    }
    if (ruleHits.length) {
      f = function(a, b) { return (a.date.getTime() >= b.date.getTime()) ?  1 : -1; }
      ruleHits.sort(f);
      currRule = ruleHits.pop().rule;
    }
    return currRule;
  },
  getAdjustedOffset: function(off, rule) {
    var save = rule[6];
    var t = this.parseTimeString(save);
    var adj = save.indexOf('-') == 0 ? -1 : 1;
    var ret = (adj*(((t[1] *60 + t[2]) * 60 + t[3]) * 1000));
    ret = ret/60/1000;
    ret -= off
    ret = -Math.ceil(ret);
    return ret;
  },
  getAbbreviation: function(zone, rule) {
    var res;
    var base = zone[2];
    if (base.indexOf('%s') > -1) {
      var repl;
      if (rule) {
        repl = rule[7];
      }
      // FIXME: Right now just falling back to Standard --
      // apparently ought to use the last valid rule,
      // although in practice that always ought to be Standard
      else {
        repl = 'S';
      }
      res = base.replace('%s', repl);
    }
    else {
      res = base;
    }
    return res;
  }
}

// Needed when talking to the XMPP server in some cases
function encode_utf8(s) {
  return unescape(encodeURIComponent(s));
}

function decode_utf8(s) {
  return decodeURIComponent(escape(s));
}
