// ==UserScript==
// @extensionName iGoogleBar
// @extensionAuthor Michael Bolin
// @extensionGUID 91b46d4b-dd86-4994-af2d-4a3fa0c18db8
// @version 0.5
// @updateURL http://www.bolinfest.com/igooglebar/
// @name iGoogleBar
// @when Pages Match
// @includes *
// ==/UserScript==


/** The HTMLDocument for the current page */
var myDocument;

/** The product whose popup is currently displayed */
var displayedProduct;

/** The product whose page the user is currently on */
var googleProduct;

/** DIV in the DOM that contains a product popup */
var div;

/** Username of the user (may be useful in constructing feed URLs */
var username;

/** Preference branch that contains the pref for the iGoogle bar */
var prefBranch;

/** <productName,divThatHoldsProductIframe */
var productToDivMap = {};

/**
 * When the page is closed, nullify all global refrences
 * to prevent memory leaks.
 */
function releaseAll() {
  myDocument = null;
  div = null;
  displayedProduct = null;
  username = null;
  googleProduct = null;
  prefBranch = null;
  for (var product in productToDivMap) {
    delete productToDivMap[product];
  }
}

/**
 * Synchronously fetch a URL
 * @param {string} url
 * @return {XMLHttpRequest}
 */
function wget(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, false);
  xhr.send(null);
  return xhr;
}

/**
 * Create HTML to display in a popup from an Atom feed
 * @param {string} atom URL to an Atom feed
 * @return {string.<HTML>} an HTML representation of the feed suitable
 *   to display in a popup
 */
function getHtmlForAtom(atom) {
  var xhr = wget(atom);
  var feedTitle = xhr.responseXML.getElementsByTagName('title')[0].
      firstChild.nodeValue;
  var entries = xhr.responseXML.getElementsByTagName('entry');
  var html = ['<h3 style="padding-bottom: 2px; margin: 0">',
      feedTitle, '</h3>'];
  if (entries.length == 0) {
    html.push('<i>Sorry, no entries at this time.</i>');
  } else {
    // For some reason, I was having problems with document.evalute(),
    // so I have to use these awful loops instead of XPath.
    for (var i = 0; i < entries.length; ++i) {
      var entry = entries[i];
      var textNode = entry.getElementsByTagName('title')[0].firstChild;
      var title = textNode ? textNode.nodeValue : '<i>no subject</i>';
      var summaries = entry.getElementsByTagName('summary');
      var summary =
          summaries.length ? summaries[0].firstChild.nodeValue : null;
      var links = entry.getElementsByTagName('link');
      var alternate;
      for (var j = 0; j < links.length; ++j) {
        var link = links[j];
        if (link.getAttribute('rel').toLowerCase() == 'alternate') {
          alternate = link.getAttribute('href');
          break;
        }
      }
      html.push(alternate
          ? '<a href="' + alternate + '" target="_blank">' + title + '</a>'
          : title,
          '<br>',
          summary ? summary : '',
          '<div style="height:6px;font-size:6px;line-height:6px;clear:both">',
          '&nbsp;</div>');
    }
  }
  return '<div style="padding: 2px 4px">' + html.join('') + '</div>';
}

var UNREADCOUNT_SUFFIX_RE = /\/state\/com\.google\/reading-list$/;

/**
 * If appropriate, return the "unread count" for a product.
 * If no count is available, return a number less than 0.
 * @param {string} product
 * @return {number.<int>} 
 */
function getUnreadCount(product) {
  try {
    if (product == 'Gmail') {
      var xhr = wget('http://mail.google.com/mail/feed/atom');
      return parseInt(xhr.responseXML.getElementsByTagName('fullcount')[0].firstChild.nodeValue);
    } else if (product == 'Reader') {
      var xhr = wget('http://www.google.com/reader/api/0/unread-count?all=true&output=json');
      var unreadcounts = eval('(' + xhr.responseText + ')').unreadcounts;
      for (var i = 0; i < unreadcounts.length; ++i) {
        var unreadcount = unreadcounts[i];
        if (UNREADCOUNT_SUFFIX_RE.test(unreadcount.id)) {
          return unreadcount.count;
        }
      }
      return -1;
    }
  } catch (e) {
    // honestly, who knows what could go wrong
    // the feed format could change, authentication could be bad, etc.
  }
  return -1;
}

var PRODUCTS = {
  Gmail : {
    icon : '//mail.google.com/mail/images/favicon.ico',
    url : '//mail.google.com/mail',
    pattern : /^https?:\/\/mail\.google\.com\/mail\//,
    atom : 'http://mail.google.com/mail/feed/atom',
    useDiv : true,
    quirks : true,
    refreshable : true,
  },
  Calendar : {
    icon : '//calendar.google.com/googlecalendar/images/favicon.ico',
    url : '//www.google.com/calendar',
    pattern : /^https?:\/\/www\.google\.com\/calendar\//,
    gadget : 'http://www.google.com/calendar/fullscreen?mode=AGENDA',
    height : 420,
  },
  Documents : {
    icon : '//docs.google.com/favicon.ico',
    url : '//docs.google.com/',
    pattern : /^https?:\/\/docs\.google\.com\//,
    gadget : 'http://docs.google.com/API/IGoogle',
    quirks : true,
  },
  Photos : {
    icon : 'http://picasa.google.com/assets/picasa.ico',
    url : '//picasaweb.google.com/home',
    pattern : /^https?:\/\/picasaweb\.google\.com\//,
    atom : 'http://picasaweb.google.com/data/feed/base/user/__USERNAME__?alt=atom',
  },
  Groups : {
    icon : '//groups.google.com//groups/img/3/favicon.ico',
    url : '//groups.google.com/',
    pattern : /^https?:\/\/groups\.google\.com\//,
    gadget : 'http://groups.google.com/?lnk=igg',
  },
  Reader : {
    icon : '//www.google.com/reader/ui/favicon.ico',
    url : '//www.google.com/reader/view/',
    pattern : /https?:\/\/www\.google\.com\/reader\//,
    gadget : 'http://www.google.com/reader/ui/standalone-module.html',
    height: 407,
    refreshable : true,
  },
  Notebook : {
    icon : '//www.google.com/notebook/images/3406433090-favicon.ico',
    url : '//www.google.com/notebook/',
    pattern : /http:\/\/www\.google\.com\/notebook\//,
    gadget : 'http://www.google.com/notebook/ig',
    noHttps : true,
  },
};

var DEFAULT_HEIGHT = 400;

function getProduct() {
  var location = window.location.toString();
  for (var product in PRODUCTS) {
    var pattern = PRODUCTS[product].pattern;
    if (pattern.test(location)) return product;
  }
  return false;
};

/**
 * @param {string} branch
 * @return {nsIPrefBranch2}
 */
function getPrefBranch(branch) {
  if (!prefBranch) {
    prefBranch = Components.classes['@mozilla.org/preferences-service;1'].
        getService(Components.interfaces.nsIPrefService).
        getBranch(branch).
        QueryInterface(Components.interfaces.nsIPrefBranch2);
  }
  return prefBranch;
}

/** default pref value for bolinfest.igooglebar */
var DEFAULT_PREF = [
  'Gmail',
  'Calendar',
  'Documents',
  'Reader',
  'Notebook',
  'Photos',
//  'Groups',
];

/**
 * Get the list of product names, in order, that should 
 * be displayed in the user's iGoogle bar.
 * This is based off of the user's bolinfest.igooglebar pref in about:config
 * @return {Array.<string>}
 */
function getMyProducts() {
  var branch = getPrefBranch('bolinfest.');
  if (branch.getPrefType('igooglebar') == 0) return DEFAULT_PREF;
  var pref = branch.getCharPref('igooglebar');
  if (!pref) {
    return DEFAULT_PREF;
  } else {
    var products = pref.split(',');
    for (var i = 0; i < products.length; ++i) {
      products[i] = Chickenfoot.trim(products[i]);
    }
    return products;
  }
}

var CALLBACK_NAME = 'iGoogleBarCallback';

var IMG_CLASS_NAME = 'iGoogleBarImageClass';

var ID_PREFIX = 'iGoogleBarId-';

var IMAGE_ID_PREFIX = 'iGoogleBarImageId-';

/**
 * Create the HTML for the iGoogle bar
 * @return {string.<HTML>} HTML for the iGoogleBar
 */
function createHtml() {
  var html = [];
  var tagName = PRODUCTS[googleProduct].useDiv ? 'div' : 'span';
  var myProducts = getMyProducts();
  for (var i = 0; i < myProducts.length; ++i) {
    var name = myProducts[i];
    var product = PRODUCTS[name];
    if (!product) continue;
    var icon = product.icon;
    if (product.noHttps) icon = 'http:' + icon;
    var label = createLabel(name);
    html.push(
      '<img class="', IMG_CLASS_NAME, '" ',
      'id="', IMAGE_ID_PREFIX, name, '" ',
      'src="', icon, '" style="cursor: pointer" ',
      'onclick="', CALLBACK_NAME, '(\'', name, '\', this)">&nbsp;',
      '<', tagName, ' class="gb1" id="', ID_PREFIX, name, '">',
      label, '</', tagName, '>');
  }
  return html.join('');
}

/**
 * @param {string} name Product name
 * @return {string.<HTML>}
 */
function createLabel(name) {
  var count = getUnreadCount(name);
  var label;
  if (count > 0 && name != googleProduct) {
    label = '<span style="font-weight: bold">' + name + ' (' + count + ')</span>';
  } else {
    label = name;
  }
  var product = PRODUCTS[name];
  var url = product.url;
  if (product.noHttps) url = 'http:' + url;
  return ((name == googleProduct) ? label
      : '<a href="' + url + '" target="_blank">' + label + '</a>');
}

function refreshLabels() {
  // if document or document.location is null,
  // then the page has been closed so stop refreshing
  if (!myDocument || !myDocument.location) {
    // remove all references in hopes of preventing a memory leak!
    releaseAll();
    return;
  }
  for (var name in PRODUCTS) {
    var product = PRODUCTS[name];
    if (!product.refreshable) continue;
    var id = ID_PREFIX + name;
    var element = myDocument.getElementById(id);
    if (!element) continue;
    element.innerHTML = createLabel(name);
  }
  scheduleRefresh();
}

function scheduleRefresh() {
  // refresh again in 5 minutes
  if (!myDocument) myDocument = document;
  setTimeout(refreshLabels, 5 * 60 * 1000);
}

function createDiv() {
  var doc = document.wrappedJSObject;
  var div = doc.createElement('div');
  div.style.top = '24px';
  div.style.left = '0';
  div.style.width = '320px';
  div.style.position = 'absolute';
  div.style.zIndex = 100000;
  if (PRODUCTS[googleProduct].quirks) {
    div.style.fontSize = 'small';
  }
  doc.body.appendChild(div);
  return div;
}

function getGbar() {
  if (!document) return null;
  return document.getElementById('gbar');
}

function getUsername() {
  if (username == null) {
    var gbar = getGbar();
    var email = find(gbar.parentNode).find(/[\w]+@gmail\.com/);
    if (email.hasMatch) {
      username = email.text.substring(0, email.text.length - '@gmail.com'.length);
    } else {
      // our heuristic failed. sad :(
      // admittedly, this won't work if the user is not using a Gmail account
      email = '';
    }
  }
  return username;
}

function callback(productName, image) {
  // get div for product
  var div;
  var isNewDiv = !(productName in productToDivMap);
  if (isNewDiv) {
    div = createDiv();
    productToDivMap[productName] = div;
  } else {
    div = productToDivMap[productName];
    if (!PRODUCTS[productName].gadget) {
      div.innerHTML = '';
    }
  }

  // position div
  div.style.left = document.getBoxObjectFor(image).x + 'px'

  // either create the IFRAME or hide it
  if (productName == displayedProduct) { 
    hideIframe(productName);
  } else if (isNewDiv || !PRODUCTS[productName].gadget) {
    var product = PRODUCTS[productName];   
    var gadget = product.gadget;
    var style = 'background-color: white; border: 1px solid #ccc; height: 420px; width: 100%;';
    style += 'height: ' + (product.height ? product.height : DEFAULT_HEIGHT) + 'px';
    if (gadget) {      
      div.innerHTML = '<iframe src="' + gadget + '" style="' + style + '"></iframe>';
    } else if (product.atom) {
      var atom = product.atom;
      var username = getUsername();
      atom = atom.replace(/__USERNAME__/g, username);
      div.innerHTML = '<div style="' + style + ' padding: 2px; overflow: auto">' +
          getHtmlForAtom(atom) + '</div>';
    } else {
      div.innerHTML = '<div style="border: 1px solid red; background-color: #FF9; padding: 2px">' +
          'Sorry, ' + productName + ' does not have an iGoogle Gadget that can be displayed here</div>';
    }
    displayedProduct = productName;
  } else {
    displayedProduct = productName;
  }
}

/**
 * @param {Node} node
 * @param {Node} ancestor
 * @return {boolean} true if node is a descendant of ancestor; false otherwise
 */
function isDescendant(node, ancestor) {
  if (!node) return false;
  if (node === ancestor || node === ancestor.wrappedJSObject) return true;
  return isDescendant(node.parentNode, ancestor);
}

function clearIframe(event) {
  var target = event.target;
  if (target.wrappedJSObject) target = target.wrappedJSObject;
  if (target.className == IMG_CLASS_NAME) {
    var productName = target.id.substring(IMAGE_ID_PREFIX.length);
    var differentProduct = (productName != displayedProduct)
    if (differentProduct) hideIframe(displayedProduct);
  } else {
    var div = productToDivMap[displayedProduct];
    if (div && !isDescendant(target, div)) {
      hideIframe(displayedProduct);
    }
  }
}

function hideIframe(productName) {
  var div = productToDivMap[productName];
  if (!div) return;
  var style = div.style;
  style.left = '-1000px';
  displayedProduct = '';
}

function main() {
  googleProduct = getProduct();
  if (!googleProduct) return;
  mainCallback();
}

var gbarAttempts = 0;

function mainCallback() {
  var gbar = getGbar();
  if (!gbar) {
    // some products, such as Gmail, do not appear to have
    // the bar readily available upon loading it, so we poll
    // once a second for 20 seconds for the bar in that case
    if (++gbarAttempts < 20) {
      setTimeout(mainCallback, 1 * 1000);
    }
    return;
  }
  var html = createHtml();
  gbar.innerHTML = '<nobr>' + html + '</nobr>';
  var win = window.wrappedJSObject;
  win[CALLBACK_NAME] = callback;
  win.document.addEventListener('mousedown', clearIframe, false);
  scheduleRefresh();
}

main();

