// ==UserScript==
// @name			LiveJournal Taglister
// @namespace		http://afunamatata.com/greasemonkey/
// @description		Fetch a taglist when you're updating your journal/editing entries
// @include			http://*.livejournal.com/update.bml*
// @include			http://*.livejournal.com/editjournal.bml*
// @tags            livejournal
// ==/UserScript==

/**
	Usage notes:	
		- now works with all journals. Thanks, ciaran_h -- you're awesome :D
		- cmd-clicking /ctrl-clicking on the tags will open the tagspage in a new tab
		- when expanded, the list shows all tags. When collapsed, it shows the top $n tags based on usage frequency, where $n is a number you define. If there are many tags tied for $nth place, it shows all of them. The top $n tags are bolded for emphasis; if you don't like it, you can get around this by setting $n to 0 or a negative number
		- there is no auto-complete, but tags that match what you type will be marked by a gray border. Also, clicking on a tag in the list should update the textbox
		-the tags list will show up in both when you update and edit. It will also automatically detect the journal you're posting to
		
	Random notes:
		- I used to get the data from the tags management page, but now I'm parsing it from http://username.livejournal.com/tag/?format=light
		- was thinking of caching the tags, but then I remembered that GM has a problem with UTF-8 encoding, and I think that certain languages require UTF-8. Would rather not risk it 
		- does not work with the script that restores the old update.bml appearance. Both of us move things around using javascript, so, um, yeah. Might be possible to integrate the two though
		- There is a hidden uglification setting built in which moves the icon to the bottom and removes music/location textfields. I've deliberately hidden it because I plan to use it, but I don't think anyone else would appreciate it
		- if anyone should want to grab any part of this and use it for any reason, say to merge with another script, then go ahead (do I even have to say that? *g*). However, if you discover any bugs, please email me at afuna@livejournal.com so I can fix it as well
		- I need to clean up my code
		
	Changelog: 
	2007.01.07 
		- fixed bug where "&" were showing as "&amp;" (thanks to turloughishere for pointing it out!)
		- changed whitespace in the tag url to a '+', in keeping with tag url conventions on LJ
		- there is no journal dropdown when you're logged out, so check whether it exists or not
	2007.01.16 
		- remembers whether you have the tagslist expanded or not
	2007.01.19 
		- reworked the code to use the journal's /tag/ page, so that it works on all journals (thanks so much to ciaran_h for the idea and the code!)		
		- add a comma when there's none at the end to make sure that tags don't merge together
		- gray box around tags that match what you're typing; bolded the top $n tags
		- clicking on a tag will replace all text up to the last comma
*/

function getArg(arg)
{
	// just in case there are other arguments
	journal = document.location.search.split("&");
	for(var i = 0; i < journal.length; ++i)
	{
		if((index = journal[i].indexOf(arg) )!= -1)
		{
			return journal[i].split("=")[1];
		}			
	}
}
function getJournal()
{
	// figure out what journal you're posting to now
	var usejournal = "";
	if(document.location.href.indexOf("update.bml") != -1)
	{
		if(document.location.search.indexOf("usejournal") != -1 )
		{
			usejournal = getArg("usejournal");
		}
		else
		{
			var journal = document.getElementById("usejournal");
			if(journal) 
			{
				usejournal = journal.value == "" ?
					document.getElementById("current_username").textContent : journal.value;
			}
		}
	}
	else	// editjournal.bml
	{
		usejournal = getArg("journal");
	}

	return usejournal;
}

function createTagItem(tag)
{
	var tagItem = document.createElement("a");
	tagItem.href="http://"+usejournal+".livejournal.com/tag/"+tag[0].replace(/ /g,'+');
	tagItem.innerHTML = tag[0];
	tagItem.freq = tag[1];
	tagItem.title = "visible: "+tagItem.freq;
	tagItem.setAttribute("match", false);
	tagItem.setAttribute("selected", false);
	tagItem.id = "taglister:"+tag[0];

	// below commented out by CH as it's handled now by autoHighlight.

//	// check whether the tags are already in the textbox
//	// still not perfect: nothing for when the user manually types in tags
//	// also, might have some false positives?
//	if(tagsTextbox.value.indexOf(tag[0] + ",") != -1 )
//	{
//		tagItem.className="selected";
//	}
	
	// do the same, but triggered by a userclick
	tagItem.addEventListener("click", function(event) {
		// don't stop propagation
		// so you can use cmd+click to open the tagspage in a new tab
		//event.stopPropagation();
		event.preventDefault();							
	
		var tagText = event.originalTarget.textContent;

		// if the tag is already in the textbox, then previously-run code will have
		// set its "selected" attribute.
		var alreadySelected = event.originalTarget.getAttribute("selected");
		alreadySelected = (alreadySelected && alreadySelected == "true");
//		var selectTag = document.getElementById("taglister:"+tagText);
//		var alreadySelected = (selectTag && (selectTag.getAttribute("match")));

//		// longer and more complicated but more robust?
//		var removeTag = new RegExp("(?:[ ]*(,)[ ]*"+tagText+"[ ]*,[ ]*"	// in-between tags 
//				+ "|^"+tagText+"[ ]*,[ ]*"
//					// beginning
//				+ "|" +tagText+"[, ]*$)");					// end
//
//		var alreadySelected = removeTag.test(tagsTextbox.value);
	
		if(alreadySelected == true)
		{
			var removeTag = new RegExp("(^|, *)"+tagText+"(?:, *|$)");
			tagsTextbox.value = tagsTextbox.value.replace(removeTag, "$1");
		}
		else
		{

			// what if it's already in the text box and you reselect it by typing?
			// if you type, don't put a comma, then decide to select the next one, it should end the most recent one with a comma, then go on
			if(tagsTextbox.value.match(/(?:^|, *)$/))
			{
				// always add no matter what
				tagsTextbox.value += tagText + ", ";
			}
			else
			{
				var match = event.originalTarget.getAttribute("match");
				if(match && match == "true")
				{
					var rindex = tagsTextbox.value.lastIndexOf(",");
					rindex = (rindex > 0 ? rindex+2 : 0);
					tagsTextbox.value = tagsTextbox.value.slice(0, rindex) + tagText + ", ";
				}			
				else // never falls through here?
				{
					tagsTextbox.value += ", " + tagText + ", ";
				}
			}
		}
		
		tagsTextbox.focus();
		highlightAll();
		
	}, true);
	return tagItem; 
}					

// called whenever we toggle from all tags to frequent tags
// also called whenever we change the value of the frequency limit
function updateFrequentlyUsed(tagNodes, toggle)
{
	freqlimit = GM_getValue("mostFrequentlyUsed",5);

	// make sure that the limit is not more than your number of tags
	freqlimit = tagNodes.length < freqlimit ? tagNodes.length: freqlimit;

	if(freqlimit <= 0)			
	{
		for( var i= 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
			tagNodes[i].style.fontWeight= "normal";
		}
		return;
	}
	/* get the most frequently used tags
		will not necessarily return *freqlimit*, may return more if
		several tags are tied for *freqlimit*th place
	*/ 
	for( var i= 0; i < tagNodes.length; ++i)
	{
		if(tagNodes[i].freq >= frequenttags[freqlimit-1][1])
		{		
			if(toggle)
				tagNodes[i].style.display = "inline";
			tagNodes[i].style.fontWeight = "bold";
		}
		else
		{
			if(toggle)
				tagNodes[i].style.display = "none";
			tagNodes[i].style.fontWeight = "normal";
		}

	}
}

function setupTagstoggle()
{
	tagslisttoggle.expanded = GM_getValue("expanded", true);
	updateFrequentlyUsed(tagNodes, true);			
	if(tagslisttoggle.expanded)
	{
		tagslisttoggle.innerHTML = "<<";
		for(var i = 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
		}
	}
	else
	{
		tagslisttoggle.innerHTML = ">>";
	}
}

// tagslisttoggle - what you click on to toggle between expanded and unexpanded taglist
function toggleTagsExpansion(tagslisttoggle)
{	
	tagslisttoggle.expanded = GM_getValue("expanded", true);

	if(!tagslisttoggle.expanded)
	{
		tagslisttoggle.innerHTML = "<<";
		for(var i = 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
		}
	}
	else
	{
		tagslisttoggle.innerHTML = ">>";
		updateFrequentlyUsed(tagNodes, true);			
	}
	
	tagslisttoggle.expanded = !tagslisttoggle.expanded;
	GM_setValue("expanded", tagslisttoggle.expanded);
}

function fetchTags()
{
usejournal = getJournal();

// reset list and fetch a new one
tagslist.innerHTML ="Fetching tags...";
// don't show toggle until there are actually tags to toggle
tagslisttoggle.innerHTML = "";


GM_xmlhttpRequest({
	method: "GET",
	url: "http://"+usejournal+".livejournal.com/tag/?format=light",	  // redirect on comm
	onload: function(details) {
		// create an IFRAME to write the document into. the iframe must be added	
		var iframe = document.createElement("IFRAME");
		iframe.style.visibility = "hidden";
		iframe.style.position = "absolute";
		iframe.style.height = "0";
		if(iframe)
		{			
			document.body.appendChild(iframe);
//			iframe.contentWindow.location.href = location.baseURI+"?usescheme=global";
			
			// write the received content into the document
			iframe.contentDocument.open("text/html");
			iframe.contentDocument.write(details.responseText);
			iframe.contentDocument.close();
			// wait for the DOM to be available, then do something with the document
			iframe.contentDocument.addEventListener("DOMContentLoaded", function() {
				/* fetch the data from the tags page */
				var ljtagiterator = iframe.contentDocument.evaluate("//ul[@class='ljtaglist']/li[text()]", iframe.contentDocument, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
				ljtaglist = new Array();
				var nextobj = ljtagiterator.iterateNext();
				while (nextobj) {
					ljtaglist.push([ nextobj.childNodes[0].innerHTML, parseInt(nextobj.lastChild.nodeValue.match(/\d+/)) ]);
					nextobj = ljtagiterator.iterateNext();
				}
				
				frequenttags = ljtaglist.slice(0);
				frequenttags.sort(function(a,b) { return (b[1] - a[1]); } );
				tagNodes = new Array();
				tagslist.innerHTML = "";				
				for (var i = 0; i < ljtaglist.length; i++) {
					temp = tagNodes.push(createTagItem(ljtaglist[i]));
					tagslist.appendChild(tagNodes[temp-1]);
					tagslist.appendChild(document.createTextNode(" "));
				}

				setupTagstoggle(tagslisttoggle);
				
				freqtext.addEventListener("change", function() {
					var freqlimit = (freqtext.value != "") ? freqtext.value : 0;
					GM_setValue("mostFrequentlyUsed", freqlimit);
					if(GM_getValue("expanded"))
					{						
						updateFrequentlyUsed(tagNodes, false);
					}
					else
					{
						updateFrequentlyUsed(tagNodes, true);
					}
				}, false);		// end CHANGE

				highlightAll();
			}, false);			// end DOMCONTENTLOADED
		}
		else
		{
			tagslist.innerHTML = "Error in fetching tags.";
		}

	}
});

var autocompletetip = document.createElement("div");
autocompletetip.style.border = "1px solid black";
autocompletetip.style.backgroundColor = "rgb(255, 255, 192)";
autocompletetip.style.position = "absolute";
autocompletetip.style.opacity = 0.8;
autocompletetip.style.display = "none";
autocompletetip.id = "autocompletetip";
var autop = document.createElement("p");
autop.style.padding = "5px";
autop.style.margin = "0";
autop.style.fontFamily = "MS Sans Serif, Arial, sans-serif";
autop.style.fontSize = "1em";
var autob = document.createElement("b");

autob.appendChild(document.createTextNode("(no tag)"));
autop.appendChild(autob);
autop.appendChild(document.createTextNode(" (press RETURN to autocomplete this)"));
autocompletetip.appendChild(autop);

var bodytag = document.getElementsByTagName("body").item(0);
bodytag.appendChild(autocompletetip);

var tagbox = document.getElementById("prop_taglist");
tagbox.addEventListener("blur", function(event) {
	autocompletetip.style.display = "none";
}, false);

tagbox.form.addEventListener("submit", autoComplete, false);
}


function trim (str) {
	return str.replace(/^\s+/, '').replace(/\s+$/, '');
}

// eventually do something more complicated
function match(tagName, current, tag)
{
	if(tagName == current)
	{
		tag.style.borderColor = "gray";
		tag.setAttribute("match", false);
		tag.className = "selected";
		tag.setAttribute("selected", true);
		return 1;
	}
	else
	{
		var index = tagName.indexOf(current);
		if(index > -1)
		{
			tag.style.borderColor = "gray";
			tag.setAttribute("match", true);
			return 1;
		}
		else
		{
			tag.style.borderColor = "transparent";
			tag.setAttribute("match", false);
			return 0;
		}
	}
}

function resetMatches()
{
	for(var i = 0; i < ljtaglist.length; ++i)
	{
		tagNodes[i].style.borderColor = "transparent";
		tagNodes[i].setAttribute("match", false);
		tagNodes[i].setAttribute("selected", false);
		tagNodes[i].className = "";
	}
}

/** 
 * Find the X position of an object, relative to the viewport 
 * Code copied from http://dev.opera.com/libraries/tooltip/docs/overview-summary-Tooltip.js.dml, copied from quirksmode.org 
 * @param obj  Object to find x position for 
 */ 
function findPosX(obj) 
{ 
	var curleft = 0; 
	if (obj.offsetParent) 
	{ 
		while (obj.offsetParent) 
		{ 
			curleft += obj.offsetLeft;
			obj = obj.offsetParent; 
		} 
	} 
	else if (obj.x) 
		curleft += obj.x; 
	return curleft; 
} 

/** 
 * Find the Y position of an object, relative to the viewport 
 * Code copied from http://dev.opera.com/libraries/tooltip/docs/overview-summary-Tooltip.js.dml, copied from quirksmode.org 
 * @param obj  Object to find y position for 
 */ 
function findPosY(obj) 
{ 
	var curtop = 0; 
	if (obj.offsetParent) 
	{ 
		while (obj.offsetParent) 
		{ 
			curtop += obj.offsetTop;
			obj = obj.offsetParent; 
		} 
	} 
	else if (obj.y) 
		curtop += obj.y; 
	return curtop; 
}

function autoHighlight()
{
	var autocomplete = highlightAll();
	// need to finish this, but the 'autocomplete' var is now the first string matched.
	var tip = document.getElementById("autocompletetip");
	if(tip)
	{
		if(autocomplete != "")
		{
			var textbox = document.getElementById("prop_taglist");
			tip.childNodes.item(0).childNodes.item(0).childNodes.item(0).nodeValue = autocomplete;
			tip.style.left = (findPosX(textbox)) + "px";
			tip.style.display = "block";
			tip.style.top = (findPosY(textbox) - tip.clientHeight - 1) + "px";   // the 1 is for the border
		}
		else
		{
			tip.style.display = "none";
		}
	}
}

function autoComplete(event)
{

	var autocomplete = highlightAll();
	var tagsTextbox = document.getElementById("prop_taglist");
	var textBoxVal = tagsTextbox.value;
//	if((!(textBoxVal.match(/, +$/))) && (autocomplete != ""))

  if(autocomplete != "")
	{
		var tagList = textBoxVal.split(/, */);
		var lasttag = tagList[(tagList.length)-1];
		var removeTag = new RegExp("(^|, *)"+lasttag+"$");
		tagsTextbox.value = tagsTextbox.value.replace(removeTag, "$1"+autocomplete+", ");
		
    event.preventDefault();
    
	}
}

function highlightAll()
{
	var textBoxVal = document.getElementById("prop_taglist").value;
	var tagList = textBoxVal.split(/, */);
	resetMatches();

	var offset = -1;
	if(textBoxVal.match(/(?:^|, *)$/))
	{
		offset = 0;
	}

	// first highlight all the stuff that's already there
	for(var i = 0; i < (tagList.length) + offset; i++)
	{
		var current = trim(tagList[i].toLowerCase());
		if(current.length > 0)
		{
			var tag = document.getElementById("taglister:"+current);
			if(tag)
			{
				tag.className = "selected";
				tag.setAttribute("selected", true);
			}
		}
	}

	var firstmatch = "";
	if(offset == -1)
	{
		// now do the last one, for which we need to highlight possibilities too.
		var lastone = trim(tagList[tagList.length-1].toLowerCase());
		if(lastone.length > 0 )
		{
			for(var i = 0; i < ljtaglist.length; ++i)
			{
				if((match(ljtaglist[i][0], lastone, tagNodes[i])) && (firstmatch == ""))
				{
					firstmatch = ljtaglist[i][0];
				}
			}
		}
	}
	return firstmatch;
}

// hidden option purely for myself (so I don't have to worry about syncing files)
function uglifyUpdateBml()
{
	loc = document.getElementById("prop_current_location");
	music = document.getElementById("prop_current_music");
	mood = document.getElementById("mood_preview");
	upic_sel = document.getElementById("userpic_select_wrapper");
	upic_img = document.getElementById("userpic");
	
	previewlabel = document.createElement("label");
	previewlabel.className="left";
	previewlabel.innerHTML="Preview:";
	
	loc.parentNode.removeChild(loc.parentNode.firstChild);
	loc.parentNode.replaceChild(upic_sel, loc);
	
	music.parentNode.removeChild(music.parentNode.firstChild);
	music.parentNode.replaceChild(previewlabel, music.parentNode.firstChild);
	music.parentNode.appendChild(upic_img);
	music.style.display = "none";
	upic_sel.removeChild(document.getElementById("lj_userpicselect"));
	upic_img.parentNode.appendChild(mood);
	
	mood.style.left = "0";
	mood.style.position="static";

}

function forceSubmit(event) {
  event.originalTarget.form.submit();
}

if(window == top && (document.getElementById("updateForm") != null ))
{		
	if(GM_getValue("uglify", false) == true)
	{
		uglifyUpdateBml();
	}
	tagNodes = new Array();
	ljtaglist = new Array();
	/*display the tagslist as a cloud of clickable tags*/
	tagslistpkg = document.createElement("p");
	tagslistpkg.id = "tagslistpkg";
	tagslistpkg.className ="pkg";

	tagslist = document.createElement("div");
	tagslist.id = "tagslist";
	
	tagslistlabel = document.createElement("label");
	tagslistlabel.className = "left";	
	tagslistlabel.innerHTML = "Tag List:"; 
	
	freqtext = document.createElement("input");
	freqtext.size = 1;
	freqtext.value = GM_getValue("mostFrequentlyUsed", 5);
	freqtext.className = "text";
	freqtext.style.cssFloat = "none";
	
	tagslisttoggle = document.createElement("a");
	tagslisttoggle.href = "";
	tagslisttoggle.addEventListener("click", function(event) {
		event.stopPropagation();
		event.preventDefault(); 

		toggleTagsExpansion(this);
	}, false);
	tagslistlabel.appendChild(document.createElement("br"));
	toplabel = document.createElement("span");
	toplabel.innerHTML = "Top ";
	toplabel.style.fontWeight = "normal";
	toplabel.style.fontSize = "small";
	tagslistlabel.appendChild(toplabel);
	tagslistlabel.appendChild(freqtext);				
	tagslistlabel.appendChild(tagslisttoggle);
	
	tagslistpkg.appendChild(tagslistlabel);
	tagslistpkg.appendChild(tagslist);
	
	tagsTextbox = document.getElementById("prop_taglist");			
	tagsTextbox.parentNode.insertBefore(tagslistpkg, tagsTextbox.nextSibling.nextSibling);

	// force contents of (nonempty) textbox to end with a comma
	var endWithComma = new RegExp("([^, ]+)[ ]*[,]?[ ]*$");
	tagsTextbox.value = tagsTextbox.value.replace(endWithComma, "$1, ");

	tagsTextbox.addEventListener("keyup", autoHighlight, false);
	/*style it, yez?*/
	GM_addStyle("#tagslistpkg a {text-decoration: none; padding: 0px 3px; border: 1px solid transparent;} #tagslistpkg a.selected {background-color: #ffffcc;} ");

  if(document.getElementsByName("action:save")[0]) {
    document.getElementsByName("action:save")[0].addEventListener("click", forceSubmit, false);
  } else {
    document.getElementsByName("action:update")[1].addEventListener("click", forceSubmit, false);
  }
	
	// fetch the tags on load	
	fetchTags();
	
	if(document.location.href.indexOf("update.bml") != -1)
	{
		// fetch the tags if you change the journal you're posting to	
		var journalselect = document.getElementById("usejournal");
		if(journalselect)
			journalselect.addEventListener("change", fetchTags, false); 
	}

}


