// ==UserScript==
// @name           Google Reader: Entry Faviconizer
// @namespace      http://arantius.com/misc/greasemonkey/
// @description    Puts favicon images next to the titles of each entry.
// @include        https://www.google.tld/reader/view/*
// @include        http://www.google.tld/reader/view/*
// @require        http://arantius.com/misc/greasemonkey/imports/dollarx.js
// ==/UserScript==

GM_addStyle((<str><![CDATA[
.entry .entry-title-link img.favicon {
	border: none;
	height: 16px;
	padding: 4px;
	vertical-align: top;
	width: 16px;
}
]]></str>).toString());

document.getElementById('entries').addEventListener(
	'DOMNodeInserted', handleNodeInserted, true
);

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

const EMPTY_PAGE_ICON='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAABGdBTUEAAK%2FINwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGsSURBVHjanJO9LgRRFMfPzFxCBNnERoIg2QYR0ajEE4hHsBKiUCg8gF40QogHoBQFjQiFKNQrsRKyivUKOzN37qd7ZuayZm8UTnLnfv5%2F5%2FzvzHhaazi7%2BbgfHiQzNIwgasUkakVB2KIkDimhURJwJkhptPzYqH9WT46qTWgPBFzevr3oP2Ln9FlfPTX19t5dbWvjuIIa23yE0JhqKAQuMAUQi2w%2BO1kCf2Fxrt4zdr25djBlz6UAzlhQBHAJIExjMpv3Eh%2FWKwxWl%2Ben%2B4fKr%2FYcwQejvwEoEiY7Vz%2BA%2FYsGuCIF8CQDaJtZZeXjGNvK0gQgxyxBHEp4eK91ALqgTYw9AqyF8ZEBYOYuEpOB93FHBYwHSHeJBYqMmJqm%2FGy%2FE8AlEUoboZf6tt5RnGbOx8KMpXYABBeEW88qE1kxzUUIlB5WqTsBSkrCHGLMjGIES7OXpGcdFUihiCiK7TgXY8%2FM3HNZkEoH1jMKWUGc2AvFV%2B2qIEmEhxUkeWbrWeTi77sx674LoJTpiQeeOUTMJ6WxV2gNIMjn6J2k3353J8D8Vf7h7jn8J74EGAB7gl%2FPOMljiAAAAABJRU5ErkJggg%3D%3D';

function hostForUrl(url) {
	var match=url.match(/:\/\/(.*?)(\/|$)/);
	if (!match) return null;

	return match[1];
}

function findFeedUrl(el) {
	var feedTitleEl=$x(
		"./ancestor::div[@class='entry-main']//a[@class='entry-source-title']",
		el
	)[0];
	if (!feedTitleEl) return '';

	// substr(18) strips the leading '/reader/view/feed/' off.
	return unescape( feedTitleEl.getAttribute('href').substr(18) );
}

function findFeedHome(text) {
	var xml=new DOMParser().parseFromString(text, "text/xml");

	function nsResolver(ns) {
		if (ns && ns in nsMap) return nsMap[ns];
		return nsMap[''];
	}
	var nsMap={'':'default'}, attr, i=0;
	while (attr=xml.documentElement.attributes.item(i++)) {
		if ('xmlns'==attr.prefix) {
			nsMap[attr.localName]=attr.value;
		} else if ('xmlns'==attr.localName) {
			nsMap['']=attr.value;
		}
	}

	var theLink=$x(
		   '/rss/channel/link/text()'
		+'| /rdf:RDF/xmlns:channel/xmlns:link/text()'
		+'| /xmlns:feed/xmlns:link[@type="text/html"]/@href',
		xml, xml, nsResolver
	);
	if (!theLink || !theLink[0]) return null

	return theLink[0].textContent;
}

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

function setIconSrc(iconEl, host) {
	if ('fail'==host) {
		iconEl.setAttribute('src', EMPTY_PAGE_ICON);
	} else if ('feedproxy.google.com'==host) {
		handleIconError({'target': iconEl});
	} else {
		iconEl.setAttribute('src', 'http://'+host+'/favicon.ico');
	}
}

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

function handleNodeInserted(event) {
	var entryEl=event.target;
	if (!entryEl.className.match(/\bentry\b/)) {
		return;
	}

	var linkEl=$x(".//a[@class='entry-title-link']", entryEl)[0];
	if (!linkEl) return;

	// Pick the host from this entry's link.
	var host=hostForUrl(linkEl.href);

	// Build an image to that host.
	var iconEl=document.createElement('img');
	iconEl.addEventListener('load', removeIconEvents, false);
	iconEl.addEventListener('error', handleIconError, false);
	iconEl.setAttribute('class', 'favicon');
	iconEl.setAttribute('tryNum', '1');

	// setTimeout() works around a GM limitation in api access checks.
	setTimeout(setIconSrc, 0, iconEl, host);

	linkEl.insertBefore(iconEl, linkEl.firstChild);
}

function removeIconEvents(event) {
	var iconEl=event.target;
	iconEl.removeEventListener('load', removeIconEvents, false);
	iconEl.removeEventListener('error', handleIconError, false);
}

function handleIconError(event) {
	function setFeedHost(host) {
		GM_setValue('feed_host_'+feedUrl, host);
		setIconSrc(iconEl, host);
	}

	var iconEl=event.target;
	var feedUrl=findFeedUrl(iconEl);

	// Prevent too many tries.  If we fail, increment the try counter.
	var tryNum=parseInt( iconEl.getAttribute('tryNum'), 10);
	tryNum++;
	iconEl.setAttribute('tryNum', tryNum);

	if (2==tryNum) {
		if (!feedUrl) return handleIconError(event);

		var feedHost=GM_getValue('feed_host_'+feedUrl, false);
		if (feedHost) {
			// Use saved feed->host mapping if we have it...
			setFeedHost(feedHost);
		} else {
			// ... otherwise fetch the feed and look it up.
			console.info('fetching', feedUrl);
			GM_xmlhttpRequest({
				'method': 'GET',
				'url': feedUrl,
				'onload': function(xhr) {
					var feedLink=findFeedHome(xhr.responseText);
					if (!feedLink) return handleIconError(event);
					setFeedHost( hostForUrl( feedLink ) );
				}
			});
		}
	} else if (3==tryNum) {
		// Set a failure state for this host.
		setFeedHost('fail');
	}
}
