// ==UserScript==
// @name          Tradzone Tagger
// @namespace     http://www.tradzone.net
// @description   add tagging to tradzone
// @include       http://www.tradzone.net/forum/viewtopic.php*
// ==/UserScript==

// Add jQuery
var GM_JQ = document.createElement('script');
GM_JQ.src = 'http://tirno.com/jquery-autocomplete.js';
GM_JQ.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(GM_JQ);

var GM_JQCSS = document.createElement('link');
GM_JQCSS.href = 'http://tirno.com/jqac.css';
GM_JQCSS.type = 'text/css';
GM_JQCSS.rel = "stylesheet";
document.getElementsByTagName('head')[0].appendChild(GM_JQCSS);


// Check if jQuery's loaded
function GM_wait() {
  if(typeof unsafeWindow.jQuery == 'undefined') { window.setTimeout(GM_wait,100); }
  else {$ = unsafeWindow.jQuery; jQuery = $; letsJQuery();  }
}

GM_xmlhttpRequest({
   method:"GET",
   url:"http://localhost:8090/",
   onload:function(response) {
     PostTagger.baseurl = "http://localhost:8090/";
     PostTagger.test = true;
     GM_log("localhost testing mode");
   }
 });

GM_wait();

PostTagger.username = ""; // access anywhere
PostTagger.title = "";
PostTagger.suggestions = [];
PostTagger.baseurl = "http://tagtradzone.appspot.com/";
PostTagger.test = false;

/* get username
   get the tags for this page
   initialise all the ui elements
   by creating a PostTagger for each of them
*/
function letsJQuery() {

  useGMxhr();

  window.setTimeout(function() {
      start_tagger()
  }, 200);
  GM_log("give a little time for baseurl to be set");

}

function start_tagger() {

  GM_log("starting with basurl: " +PostTagger.baseurl);

  var userlogout = $("a.mainmenu:last").text();
  if (userlogout == "Connexion") {
    GM_log("User not logged in");
    return;
  }
  PostTagger.username = /\[\s(.*)\s\]/(userlogout)[1];
  GM_log("Username: " + PostTagger.username);

  PostTagger.title = $("a.maintitle").text();
  GM_log("Title: " + PostTagger.title);

  var postdetails = $("span.postdetails"); 
  var tds = postdetails.filter(":odd").parent();
  var authors = postdetails.filter(":even").parent().map(getAuthor).get();
  var ids = tds.map(getPostId).get();

  getAllTags(function(val) {allTagsGot(val,tds,ids, authors)});
}
// continuation will resume here
function allTagsGot(suggestions,tds,ids, authors) {
  PostTagger.suggestions = suggestions;
  getTagsFor(ids, function(val) {tagsForGot(val,tds,ids, authors)});
}
// continuation will resume here
function tagsForGot(tagsforids,tds,ids, authors) {
  tds.each(function(i) {
      var td = $(this).nextAll("td:first"); // td containing the buttons
      var tr = $(this).parent().nextAll("tr:first").children("td"); // tr between post header and post
      var div = tr.prepend("<div></div>").children(":first");
      var id = ids[i];
      var author = authors[i];
      new PostTagger(id,author,div,td,tagsforids[id]);
    });
  GM_log("Added tagging functionalities to "+ tds.length +" posts.");
};

PostTagger.taggers = new Array();

function PostTagger(postid, author,uiparent,buttonparent,tags) {
  this.postid = postid;
  this.author = author;
  this.tags = [];
  this.ui = this.createTagInterface(uiparent);
  this.button = this.createTagLink(buttonparent);
  this.isvisible = true;

  this.addTags(tags);

  if (this.tags.length == 0) { // make invisible if no tags yet
    this.toggleUI(false);
  }

  PostTagger.taggers.push(this); // prevent garbage collection? or is it not necessary?
};

PostTagger.prototype.toggleUI = function(animate) {
  speed = animate ? 250 : 0;
  if (this.isvisible) {
    this.isvisible=false;
    $(this.ui).hide(speed);
  } else {
    this.isvisible=true;
    $(this.ui).show(speed);
  }
};

PostTagger.prototype.createTagLink = function(parent) {
  var link = $("<a>tagger</a>");
  link.data("tagger",this); // needed for callbacks
  link.addClass("taggerlink");
  if (PostTagger.test) {
    link.addClass("test");
  }
  link.click(function() {
      $(this).data("tagger").toggleUI(true);
    });
  $(parent).prepend(link);
  return link;
};

PostTagger.prototype.createTagInterface = function(parent) {
  parent = $(parent);

  var tags = $("<span>pas encore de tags</span>");
  tags.addClass("taggertags").addClass("taggertext");
  parent.append(tags);

  var input = $("<input type='text'></input>");
  input.autocomplete({ get : get_tag_suggs,
	multi: true,
	minchars: 1,
	delay: 100,
	cache: false});
  
  // get the keydown handler bound by autocomplete 
  var jqac_keydown = input.data("events")["keydown"];
  for (var k in jqac_keydown) {
    jqac_keydown = jqac_keydown[k];
    break; // only take the first one
  }
  input.unbind("keydown");

  // bind our own handler which is calls the autocomplete handler
  // and assigns the tags when return is pressed
  input.data("tagger",this); // needed for callbacks
  input.bind("keydown",function(ev){
      if (jqac_keydown.apply(this,[ev])) {
	  switch(ev.which){
	  case 13:
	    var input = $(this);
	    var tagger = input.data("tagger");
	    var id = tagger.postid;
	    var author = tagger.author;
	    var tagnames = input.attr("value");
	    input.attr("value","");
	    input.trigger("keyup"); // clear the selection list
	    setTagsFor(id, author, PostTagger.title, PostTagger.username, tagnames, 
		       function(tagnames) {
			 tagger.addTags(tagnames);
			 for (var i=0; i<tagnames.length; i++) {
			   addToSuggestions(tagnames[i]);
			 }
		       });
	  }
	  return true;
      }
      return false;
  });

  var label = $("<span>ajouter:</span>").addClass("taggertext").addClass("taggeradd");
  parent.append(label);
  parent.append(input);
  return parent;
};

PostTagger.prototype.addTags = function(tagnames) {
  for (var i in tagnames) {
    this.addTag(tagnames[i]);
  }
}

PostTagger.prototype.addTag = function(tagname) {
  // create the tag html
  var tag = $("<a>"+tagname+"</a>").attr("href",getTagUrl(tagname));

  // get insertion place
  var span = $(this.ui).children(":first");

  if (this.tags.length == 0) {
    span.text("tags: "); // first tag
  } else {
    span.append(", "); // subsequent tags
  }
  span.append(tag);
  this.tags.push(tagname);
}

function get_tag_suggs(v){ 
   var a=[];
   var s=PostTagger.suggestions;
   myRegExp = new RegExp("^"+v, "i")  
   for(var category in s) {
     var tags = s[category];
     var matchcategory = myRegExp.test(category);
     for (var i=0; i<tags.length; i++) {
       if (matchcategory) {
	 a.push({id:category+i, value:tags[i], info:category})
       } else if (myRegExp.test(tags[i])) {
	 a.push({id:category+i, value:tags[i]});
       }
     }
   }
   return a;
}

function getTagUrl(tagname) {
  return "http://fr.wikipedia.com/wiki/"+tagname;
}

function getPostId() {
  // link to original post is in the first child of the TD
  // looks like "viewtopic.php?p=120178#120178"
  // get the part between = and #
  var href = $(this).children(":first").attr("href");
  return /=(\d*)#/(href)[1]; 
};

function getAuthor() {
  return $(this).children(":first").find("b a").text();
}

/** get all the tags for the specified ids
 *
 * @param ids an array of ids such as ["12355","23423"] etc.
 * @return tags for each id such as {"12355":[], "23423":["tag1","tag2"]}
*/
function getTagsFor(ids, cont) { // this call will be synchronous (simulated by continuation)
  GM_log("getTagsFor: "+ids.join(","));

  GM_log("sending ajax for get tags")
  $.ajax({
    type: "GET",
	url: PostTagger.baseurl+"json/gettags",
	data: {ids: ids.join(",")},
	dataType: "text", // don't mention it's json otherwise jquery looks at the url and thinks "baaah"
	success: function(responsestring) {
	GM_log("response: "+ responsestring);
	eval("var tagarray =("+responsestring+")");
	     GM_log("found  tags"+tagarray);
	     cont(tagarray);
      }
    });
}; 

/** Set the tags for the specified id
 *
 * @param id String of the id such as "12355"
 * @param user String of the user such as "Tirno"
 * @param tags String of tags such as "tag1, tag2, tag3"
 * @param cont Continuation function to be called on success with parameter: an array of assigned tags 
 * @return void 
 */
function setTagsFor(id, author, title, user, tags, cont) { // this call will be async.
  GM_log("setTags: "+[id,author, title, user,tags,cont].join(", "));

  // fill in ajax call here, return tags that have been assigned
  //tagarray = tags.split(/,\s*/);
  GM_log("sending ajax")
  $.ajax({ 
    type: "POST",
	url: PostTagger.baseurl+"json/addtags",
	data: {id:id, author:author,title:title,user:user,tags:tags},
	dataType: "text", // don't mention it's json otherwise jquery looks at the url and thinks "baaah"
	success: function(responsestring) {
	eval("var tagarray =("+responsestring+")");
	     GM_log("tagarray"+tagarray);
	     cont(tagarray);
      }
    });



  // call continuation function with the assigned tag array
  // TODO add in display of tags that were not assigned for whatever reason.
  // TODO add in "busy indicator"
  //cont(tagarray);
};

function getAllTags(cont) { // do this with remote call for now (while in greasemonkey). simulated synchronous
  var tags = {"instruments":["cornemuse","flute", "violon", "accordeon", "clarinette", "concertina"],
	      "danses":     ["mazurka","bourrée","polka","cercle circassien","valse","jig","fandango"],
	      "festivals":  ["damada","gennetines","p'tit festival","boombal"],
	      "groupes":    ["komred","bardane","tradirrationnel"],
	      "":           ["idée","opinion","explication","viellux","danse","enseignement",
		  "partition","mode","solfege"]
  };
  cont(tags);
};

/** add the array of tags to PostTagger.suggestions 
 * Performs check to see if they are already included
 * TODO perform check serverside?
 */
function addToSuggestions(newtag) {
  var s=PostTagger.suggestions;
  for(var category in s) {
    var tags = s[category];
    for (var i=0; i<tags.length; i++) {
      if (tags[i]==newtag) return;
    }
  }
  PostTagger.suggestions[""].push(newtag);
}


























// code that follows is for using xmlhttprequest in greasemonkey

function unsafeXMLHttpRequest() {
  var self = this;
    
  self.headers = null;

  self.open = function(_method, _url, _type, _username, _password) {

    if (_type!=null && !_type) {
      GM_log("No support for synchronous requests, please use a continuation");
    }

    self.method = _method;

    if (!_url.match(/^http/)) {
      GM_log("You need to specify absolute urls when using the 'XMLHttpRequest - Security Bypass' script");
      throw("You need to specify absolute urls when using the 'XMLHttpRequest - Security Bypass' script");
    }

    self.url = _url;
  }

  self.setRequestHeader = function(_header, _value) {
    if (self.headers == null) {
      self.headers = new Object();
    }
    self.headers[_header] = _value;
  }

  self.send = function(_data) {
    self.oldOnload = self.onload;
    self.oldOnerror = self.onerror;
    var oldOnreadystatechange = self.onreadystatechange;

    self.data = _data;

    self.onload = function(responseDetails) {
      copyValues(responseDetails);
      if (self.oldOnload) { self.oldOnload(); }
    }

    self.onerror = function(responseDetails) {
      copyValues(responseDetails);
      if (self.oldOnerror) { self.oldOnerror(); }
    }

    self.onreadystatechange = function(responseDetails) {
      copyValues(responseDetails);
      if (oldOnreadystatechange ) { oldOnreadystatechange(); }
    }


    function copyValues(responseDetails) {
      if (responseDetails.readyState) {
	self.readyState = responseDetails.readyState;
      }

      if (responseDetails.status) {
	self.status = responseDetails.status;
      }

      if (responseDetails.statusText) {
	self.statusText = responseDetails.statusText;
      }

      if (responseDetails.responseText) {
	self.responseText = responseDetails.responseText;
      }
      if (responseDetails.responseHeaders) {
	self.responseHeaders = responseDetails.responseHeaders;
      }
//       if (self.responseText) { 
// 	GM_log("adding response xml")
// 	try {
// 	  var dp = new XPCNativeWrapper(window, "DOMParser()");
// 	  var parser = new dp.DOMParser();
// 	  self.responseXML = parser.parseFromString(self.responseText, 'text/xml');
// 	} catch (ex) { GM_log("xml parse failed") }
	
//       }
    }
window.setTimeout(function() {
    GM_xmlhttpRequest(self);
  }, 0);
    
  }
    
  self.abort = function() {
    GM_log("XMLHttpRequest - Bypass Security doesn't support the 'abort' method yet.");
  }
    
  self.getAllResponseHeaders = function() {
    GL_log("XMLHttpRequest - Bypass Security doesn't support the 'getAllResponseHeaders' method yet.");
  }
    
  self.getResponseHeader = function(_header) {
    var headers = self.parseHeaders(self.responseHeaders);
    var value = headers[_header];
    return value;
  }
    
  self.overrideMimeType = function() {
    GM_log("XMLHttpRequest - Bypass Security doesn't support the 'overrideMimeType' method yet.");
  }

  self.parseHeaders = function (headers) {
    if (self._responseHeaders) { return self._responseHeaders; }

    var ret = new Array();
    var lines = headers.split("\n");
    for (var i = 0; i < lines.length; i++) {
      var sepIndex = lines[i].indexOf(':');
      if (sepIndex > 0) {
	var header = lines[i].substring(0, sepIndex);
	var value = lines[i].substring(sepIndex + 1);
	value = value.replace(/^\s+/, '');
	ret[header] = value;
      }
    }

    self._responseHeaders = ret;
    return ret;
  }
}

function useGMxhr () {
  GM_log("using gmxhr")
  $.extend({
	ajax: function( s ) {
		var jsonp, jsre = /=\?(&|$)/g, status, data;

		// Extend the settings, but re-extend 's' so that it can be
		// checked again later (in the test suite, specifically)
		s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));

		// convert data if not already a string
		if ( s.data && s.processData && typeof s.data != "string" )
			s.data = jQuery.param(s.data);

		// Handle JSONP Parameter Callbacks
		if ( s.dataType == "jsonp" ) {
			if ( s.type.toLowerCase() == "get" ) {
				if ( !s.url.match(jsre) )
					s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
			} else if ( !s.data || !s.data.match(jsre) )
				s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
			s.dataType = "json";
		}

		// Build temporary JSONP function
		if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
			jsonp = "jsonp" + jsc++;

			// Replace the =? sequence both in the query string and the data
			if ( s.data )
				s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
			s.url = s.url.replace(jsre, "=" + jsonp + "$1");

			// We need to make sure
			// that a JSONP style response is executed properly
			s.dataType = "script";

			// Handle JSONP-style loading
			window[ jsonp ] = function(tmp){
				data = tmp;
				success();
				complete();
				// Garbage collect
				window[ jsonp ] = undefined;
				try{ delete window[ jsonp ]; } catch(e){}
				if ( head )
					head.removeChild( script );
			};
		}

		if ( s.dataType == "script" && s.cache == null )
			s.cache = false;

		if ( s.cache === false && s.type.toLowerCase() == "get" ) {
			var ts = (new Date()).getTime();
			// try replacing _= if it is there
			var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
			// if nothing was replaced, add timestamp to the end
			s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
		}

		// If data is available, append data to url for get requests
		if ( s.data && s.type.toLowerCase() == "get" ) {
			s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;

			// IE likes to send both get and post data, prevent this
			s.data = null;
		}

		// Watch for a new set of requests
		if ( s.global && ! jQuery.active++ )
			jQuery.event.trigger( "ajaxStart" );

		// If we're requesting a remote document
		// and trying to load JSON or Script with a GET
		if ( (!s.url.indexOf("http") || !s.url.indexOf("//")) && s.dataType == "script" && s.type.toLowerCase() == "get" ) {
			var head = document.getElementsByTagName("head")[0];
			var script = document.createElement("script");
			script.src = s.url;
			if (s.scriptCharset)
				script.charset = s.scriptCharset;

			// Handle Script loading
			if ( !jsonp ) {
				var done = false;

				// Attach handlers for all browsers
				script.onload = script.onreadystatechange = function(){
					if ( !done && (!this.readyState || 
							this.readyState == "loaded" || this.readyState == "complete") ) {
						done = true;
						success();
						complete();
						head.removeChild( script );
					}
				};
			}

			head.appendChild(script);

			// We handle everything using the script element injection
			return undefined;
		}

		var requestDone = false;

		// Create the request object; Microsoft failed to properly
		// implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
		var xml = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new unsafeXMLHttpRequest();

		// Open the socket
		xml.open(s.type, s.url, s.async, s.username, s.password);

		// Need an extra try/catch for cross domain requests in Firefox 3
		try {
			// Set the correct header, if data is being sent
			if ( s.data )
				xml.setRequestHeader("Content-Type", s.contentType);

			// Set the If-Modified-Since header, if ifModified mode.
			if ( s.ifModified )
				xml.setRequestHeader("If-Modified-Since",
					jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );

			// Set header so the called script knows that it's an XMLHttpRequest
			xml.setRequestHeader("X-Requested-With", "XMLHttpRequest");

			// Set the Accepts header for the server, depending on the dataType
			xml.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
				s.accepts[ s.dataType ] + ", */*" :
				s.accepts._default );
		} catch(e){}

		// Allow custom headers/mimetypes
		if ( s.beforeSend )
			s.beforeSend(xml);
			
		if ( s.global )
			jQuery.event.trigger("ajaxSend", [xml, s]);

		// Wait for a response to come back
		var onreadystatechange = function(isTimeout){
			// The transfer is complete and the data is available, or the request timed out
			if ( !requestDone && xml && (xml.readyState == 4 || isTimeout == "timeout") ) {
				requestDone = true;
				
				// clear poll interval
				if (ival) {
					clearInterval(ival);
					ival = null;
				}
				
				status = isTimeout == "timeout" && "timeout" ||
					!jQuery.httpSuccess( xml ) && "error" ||
					s.ifModified && jQuery.httpNotModified( xml, s.url ) && "notmodified" ||
					"success";

				if ( status == "success" ) {
					// Watch for, and catch, XML document parse errors
					try {
						// process the data (runs the xml through httpData regardless of callback)
						data = jQuery.httpData( xml, s.dataType );
					} catch(e) {
						status = "parsererror";
					}
				}

				// Make sure that the request was successful or notmodified
				if ( status == "success" ) {
					// Cache Last-Modified header, if ifModified mode.
					var modRes;
					try {
						modRes = xml.getResponseHeader("Last-Modified");
					} catch(e) {} // swallow exception thrown by FF if header is not available
	
					if ( s.ifModified && modRes )
						jQuery.lastModified[s.url] = modRes;

					// JSONP handles its own success callback
					if ( !jsonp )
						success();	
				} else
					jQuery.handleError(s, xml, status);

				// Fire the complete handlers
				complete();

				// Stop memory leaks
				if ( s.async )
					xml = null;
			}
		};
		
		if ( s.async ) {
			// don't attach the handler to the request, just poll it instead
			var ival = setInterval(onreadystatechange, 13); 

			// Timeout checker
			if ( s.timeout > 0 )
				setTimeout(function(){
					// Check to see if the request is still happening
					if ( xml ) {
						// Cancel the request
						xml.abort();
	
						if( !requestDone )
							onreadystatechange( "timeout" );
					}
				}, s.timeout);
		}
			
		// Send the data
		try {
			xml.send(s.data);
		} catch(e) {
			jQuery.handleError(s, xml, null, e);
		}
		
		// firefox 1.5 doesn't fire statechange for sync requests
		if ( !s.async )
			onreadystatechange();

		function success(){
			// If a local callback was specified, fire it and pass it the data
			if ( s.success )
				s.success( data, status );

			// Fire the global callback
			if ( s.global )
				jQuery.event.trigger( "ajaxSuccess", [xml, s] );
		}

		function complete(){
			// Process result
			if ( s.complete )
				s.complete(xml, status);

			// The request was completed
			if ( s.global )
				jQuery.event.trigger( "ajaxComplete", [xml, s] );

			// Handle the global AJAX counter
			if ( s.global && ! --jQuery.active )
				jQuery.event.trigger( "ajaxStop" );
		}
		
		// return XMLHttpRequest to allow aborting the request etc.
		return xml;
	}
      
      
    });
}
