//
// 	Chirrup.js
//

// 	ChirrupUI
//
/*	This class is used to insert a fresh commenting UI into your HTML document.
		Typically you would include this after a post on your blog, or at the bottom
		of the page that you wish to comment.
		
		Important note about URLs: Chirrup does not recognise ANY of these as being the
		same url:
			- http://myblog.com/article
			- http://myblog.com/article#comments
			- http://myblog.com/article?page=2
		
		This is because Chirrup does not know if you consider ?page=2 to be a different page
		that requires different comments. That kind of thing varies wildly from site to site,
		so Chirrup doesn't take a position on the matter. Anchors such as #myheading are less
		controversial, but you may actually want comments for individual parts of your page.
				
		For that reason it is up to you to define what you think is a distinct URL in your
		site and to pass the correct URL to chirrup.
		
		Usage:
		
		new ChirrupUI(
			string chirrup_url, // the URL of your Chirrup installation.
			string page_url,		// the URL of the page you want to show the comments for
			object options			// a hash of options, available as follows:
													// 
		)
*/
//

// Out-of-class counter for allocation of working ID's for each Chirrup block in a page.
ChirrupUIController = function() {
	this.init();
}
ChirrupUIController.prototype = {
	
	init: function() {
		this.instances = [];
		this.instanceCounter = 0;
	},
	
	allocIDForInstance: function(instance) {
		key = this.instanceCounter;
		this.instanceCounter += 1;
		this.instances[key] = instance;
		return key;
	},
	
	getInstanceForID: function(id) {
		return this.instances[id];
	}	
	
};
$ChirrupUIController = new ChirrupUIController();

// Define class
ChirrupUI = function(chirrup_url, page_url, data, options) {
	this.init(chirrup_url, page_url, data, options);
}
ChirrupUI.prototype = {
	
	// Static config
	consoleSpam: true, // Use verbose logging to the browser's console?
		
	// Constructor
	init: function(chirrup_url, page_url, data, options) {
		// Store config
		this.data = data;
		this.chirrup_url = chirrup_url;
		this.page_url = page_url;
		this.options = jQuery.extend({
			enablePosting: false, 	// Should Chirrup render a form into the UI block?
			language: "en",				// Currently only "en" is supported but hey, contributions are welcome
			startWith: "comments",// Should this UI block show up starting with existing comments or with the post form?
														// specify "comments" or "form" to set this option.
			crossDomain: true,		// Particularly important. If the page_url is on a different domain to the chirrup_url, we cannot
														// allow the post form to submit via AJAX as it will fail silently with a security error.
														// If crossDomain is TRUE, the form will submit normally.
			twitterCharLimit: 140
		}, (options || {}));
		
		// Store shortcut variables such as commonly-used node ID's
		this.id = $ChirrupUIController.allocIDForInstance(this);
		this.root_id = "chirrup_"+this.id+"_root";		
		
		this.build();
		return this;
	},
	
	// Draws the UI block into the page and starts the comments loading.
	build: function() {
		// Inserts a hook element containing a loading message. This hook element will be the container for the hook element loaded from the Chirrup service.
		var root_element = document.createElement('div');
		root_element.setAttribute('id', this.root_id);
		root_element.setAttribute('class', 'chirrup chirrup_root chirrup_loading');
		var p_element = document.createElement('p');
		p_element.setAttribute('class', 'chirrup_loading');
		p_element.innerHTML = this.l('loading_message');
		root_element.appendChild(p_element);
		document.getElementById('twitter-mentions').appendChild(root_element);
		//document.write("<div id=\""+this.root_id+"\" class=\"chirrup chirrup_root chirrup_loading\"><p class=\"chirrup_loading\">"+this.l("loading_message")+"</p></div>");		
		this.drawUIWithReceivedData(this.data);
	},
	
	// Called when the root element has been drawn into the document, which we'll use as a hook for the rest of the render.
	drawUIWithReceivedData: function(data) {
		// Save data
		this.data = data;
		// Draw UI
		var id = this.id;
		var root_id = "chirrup_"+id+"_root";
		var form_id = "chirrup_"+id+"_form";
		var list_id = "chirrup_"+id+"_list";
		var root_context = this.rootContext();
		
		// Remove the loading flag, giving us a blank slate
		jQuery("#"+root_id+" p.chirrup_loading").remove();
		
		// Insert the switching block
		/*jQuery("<ul />").attr(
			{"class": "chirrup_navigation"}
		).append(
			jQuery("<li />").attr(
				{"id": "chirrup_"+id+"_navigate_comments",	"class": this.activityClassForComments()}
			).append(
				jQuery("<a />").attr(
					{"href": "#chirrup_"+id+"_navigate_comments"}
				).html(
					this.l("navigation_comments")+" ("+data.statuses.length+")")
				).bind("click", {"id": id}, function(e) {
					$ChirrupUIController.getInstanceForID(e.data.id).switchUI("comments");
					return false;
				})
			).append(
				jQuery("<li />").attr({id: "chirrup_"+id+"_navigate_form", "class": this.activityClassForForm()}
			).append(
				jQuery("<a />").attr(
					{"href": "#chirrup_"+id+"_navigate_form"}
				).html(
					this.l("navigation_form")
				).bind("click", {"id": id}, function(e) {
					$ChirrupUIController.getInstanceForID(e.data.id).switchUI("form");
					return false;
				})
			)
		).prependTo(root_context);*/
		
		
		// Insert the posting form
		if(this.options["enablePosting"]) {
						
				// Write the form block into the DOM for manipulation
				jQuery(this.templates["form"]).appendTo(root_context);
				// Set up the form properties
				jQuery("form", root_context).attr({
					method: "post",
					action: this.chirrup_url+"?post=1&id="+id+"&url="+escape(this.page_url),
					id: form_id,
					"class": this.activityClassForForm()
				}).bind("submit", {"id": this.id}, function(e) {
					return $ChirrupUIController.getInstanceForID(e.data.id).postFormWantsToSubmit();
				});
				// Add contents to the template
				jQuery("legend", root_context).html( this.l("form_legend") );
				// Login + comment fields
				// For the record, IE is to blame for the sodding inline attributes on the input elements.
				// Internet Explorer, from hell's heart I stab at thee.
				field_context = jQuery("dl", root_context);
					// Login
					field_context.append(		jQuery("<dt />").append(		jQuery("<label />").attr("for", "chirrup_"+id+"_field_username").append( this.l("username_label") )));
					field_context.append(		jQuery("<dd />").append(		"<input type=\"text\" id=\"chirrup_"+id+"_field_username\" name=\"username\" />"));
					// Password
					field_context.append(		jQuery("<dt />").append(		jQuery("<label />").attr("for", "chirrup_"+id+"_field_password").append( this.l("password_label") )));
					field_context.append(		jQuery("<dd />").append(		"<input type=\"password\" id=\"chirrup_"+id+"_field_password\" name=\"password\" />"));
					// Descriptive
					field_context.append(		jQuery("<dt />").append(		jQuery("<span />").attr("class", "chirrup_meta_label").append("Chirrup")		));
					field_context.append(		jQuery("<dd />").append(		jQuery("<p />").attr("class", "chirrup_security_note").append( this.l("post_explanation")+" <span class=\"chirrup_preview_body\">"+this.tweetTemplateForData(this.data)+"</span>" )).append(	jQuery("<p />").attr("class", "chirrup_charcounter") ).append( jQuery("<p />").attr({"class": "chirrup_preview", "style": "display: none;"}) ).attr("class", "chirrup_post_explanation_def")	);
					// Comment area
					field_context.append(		jQuery("<dt />").append(		jQuery("<label />").attr("for", "chirrup_"+id+"_field_comment").append( this.l("comment_label") )));
					field_context.append(		jQuery("<dd />").append(		"<textarea onkeyup=\"$ChirrupUIController.getInstanceForID("+id+").postTextareaValueDidChange()\" onblur=\"$ChirrupUIController.getInstanceForID("+id+").postTextareaValueDidChange()\" onfocus=\"$ChirrupUIController.getInstanceForID("+id+").postTextareaValueDidChange('focus')\" id=\"chirrup_"+id+"_field_comment\" name=\"comment\" class=\"unaltered\" rows=\"6\" cols=\"40\">"+this.l("post_default")+"</textarea>").addClass("chirrup_comment_field_wrapper"));
				// Submit button
				jQuery("fieldset:last", root_context).append("<input type=\"submit\" value=\""+this.l("submit_label")+"\" />").addClass("submit");
		}
		// Insert the empty list
		jQuery("<ol />").attr({
			id: list_id,
			"class": this.activityClassForComments()
		}).appendTo(root_context);
		
		// Now populate the comment list
		this.updateUIWithReceivedData(data);
		
		// At this point, pick up a status flag in the URL in case we're coming back from a crossdomain POST.
		var dl = new String(document.location);
		if(dl.match("chirrup_"+this.id+"_post_result=1")) {
			// Oh, we just came back from a successful post.
			this.switchUI("comments");
			this.postCommentRequestFinished({
				"block_id": this.id,
				"result": 1
			});
		} else if(dl.match("chirrup_"+this.id+"_post_result=0")) {
			// Ah, post just failed.
			this.switchUI("form");
			this.postCommentRequestFinished({
				"block_id": this.id,
				"result": 0
			});
		}
		
		// Insert the attribution
		/*jQuery("<p />").attr("class", "chirrup_attribution").html(this.l("chirrup_attribution")).appendTo(root_context);*/
			
		return false;
	},
	
	// Updates the status list within the UI without updating the UI as a whole.
	// WARNING: runs outside of instance scope binding
	updateUIWithReceivedData: function(data) {
		var id = data.block_id;
		var inst = $ChirrupUIController.getInstanceForID(id);
		var root_context = this.rootContext();
		var list_context = this.listContext();
		
		// Store the data
		this.data = data;
		
		// Mark the 'comments' navigation with the message count
		jQuery("#chirrup_"+id+"_navigate_comments a").html(	
			this.l("navigation_comments")+" ("+data.statuses.length+")"
		);
		
		// Blank the list for rendering
		list_context.html("");
		
		// Render an 'empty' state if the array is empty
		if(data.statuses.length ==0) {
			jQuery("<li class=\"chirrup_no_comments\" />").html(this.l("no_comments")).prependTo(list_context);
			this.switchUI("form");
		}
		
		// Now render the status list fresh
		for(i=0; i<data.statuses.length && i < 10 /* XXX */; i++) {
			s = data.statuses[i];
			status_context = jQuery(this.templates["status"]).appendTo(list_context);
			status_id = "chirrup_"+id+"_status_"+s.id;
		
			citation_link_attrs = {
				href: "http://twitter.com/"+s.user.screen_name,
				rel: "external"
			};
			// Add properties to the list item
			status_context.attr({
				"class": "chirrup_status",
				id: status_id
			});
			// Append an 'alternate_row' class to odd rows - this is done counting backwards
			// so that as new messages arrive, old messages will keep their stripe colour.
			r_key = (data.statuses.length-1)-i;
			if(r_key % 2 == 1) status_context.addClass("alternate_row");
			
			// Add a 'first' or 'last' class if appropriate
			if(i==0) status_context.addClass("first");
			if(i==data.statuses.length-1) status_context.addClass("last");
			
			// Insert status
			//jQuery("blockquote p", status_context).prepend(this.processStatusText(s.text));
			jQuery("p", status_context).prepend(this.processStatusText(s.text));
			// Insert citation
			//jQuery("blockquote cite", status_context).prepend(s.created_at_human+" by ");
			jQuery("span cite", status_context).prepend("(" + s.created_at_human + ")");
			//jQuery("blockquote cite a", status_context).attr(citation_link_attrs).html(s.user.screen_name);
			jQuery("span a.tuser", status_context).attr(citation_link_attrs).html(s.user.screen_name);
			
			// Update image
			jQuery("a:first", status_context).attr(citation_link_attrs).addClass("chirrup_avatar_link")
			jQuery("a:first img", status_context).attr({
				"src": s.user.profile_image_url,
				"class": "chirrup_avatar",
				"alt": s.user.name
			});
		}
		return false;
	},
	
	// Triggered when the value in the textarea has changed.
	postTextareaValueDidChange: function(event_type) {
		this.commentFieldTouched = true;
		var field = jQuery("#chirrup_"+this.id+"_field_comment");
		var default_value = this.tweetTemplateForData(this.data);
		var essentials = default_value.split(this.l("post_default"))[0];
		var val = field.val();
		
		var root_context = jQuery("#"+this.root_id);
		
		// Hide the instruction block
		jQuery(".chirrup_security_note", root_context).hide();
		
		// Show and update the preview
		var preview = jQuery(".chirrup_preview", root_context);
				preview.html("<span class=\"chirrup_preview_body\">"+essentials+field.val()+"</span>");
		
		// Remove default value; we focussed the field, it's not needed any more
		field.val( field.val().replace(this.l("post_default"), ""));
		// Character counter
		//alert("-"+essentials+"-");
		this.drawCharCounter(this.options.twitterCharLimit - (field.val().length+essentials.length));
	},
	
	// Triggered when the post form wants to submit. Does a little pre-posting validation and sends the tweet if things check out.
	postFormWantsToSubmit: function() {
		// Do pre-validation
		var root_context = this.rootContext();
		var default_value = this.tweetTemplateForData(this.data);
		var essentials = default_value.split(this.l("post_default"))[0];
		this.clearValidations();		
				
		username_field = jQuery("#chirrup_"+this.id+"_field_username", root_context);
			if(username_field.val().match(/^\s*$/)) {
				this.validation_failures = true;
				this.addValidationErrorToField(username_field, this.l("validation_blank_username"));
				username_field.addClass("chirrup_errors");
			}
		password_field = jQuery("#chirrup_"+this.id+"_field_password", root_context);
			if(password_field.val().match(/^\s*$/)) {
				this.validation_failures = true;
				this.addValidationErrorToField(password_field, this.l("validation_blank_password"));
				password_field.addClass("chirrup_errors");
			}
		comment_field = jQuery("#chirrup_"+this.id+"_field_comment", root_context);
			// The comment field must not be too long
			if(essentials.length+comment_field.val().length > this.options.twitterCharLimit) {
				this.validation_failures = true;
				this.addValidationErrorToField(comment_field, this.l("validation_comment_too_long"));
				comment_field.addClass("chirrup_errors");				
			}
			// The user should have entered a comment
			if(!this.commentFieldTouched || comment_field.val().replace(" ", "").length == 0) {
				this.validation_failures = true;
				this.addValidationErrorToField(comment_field, this.l("validation_no_comment_entered"));
				comment_field.addClass("chirrup_errors");
			}
			
		if(!this.validation_failures) {
			// No validation errors. Start submitting the form.
			// The comment field must have the essentials at the very start. Conditionally inject them now.
			var v = comment_field.val();
			if(v.indexOf(essentials) != 0) comment_field.val(
				essentials+comment_field.val()
			);
			// Disable the form input
			jQuery("fieldset.submit input", root_context).attr("disabled", "disabled").val(this.l("submit_label_progress"));
			
			// Deal with possible cross-domain use
			if(this.options.crossDomain) {
				// If we're submitting cross-domain, the form needs to post normally.
				// Just return true to let the form do it's thing.
				return true;
			} else {
				// If we're not submitting cross-domain, we can do fancy AJAX stuff.
				jQuery.post(this.chirrup_url+"?post=1&id="+this.id+"&give=json&url="+this.page_url, jQuery("form", root_context).serialize(), this.postCommentRequestFinished, "json");
				// Don't return true here - fall back to the false to prevent the form from firing normally.
			}
		}		
		// We didn't return true already, so return false and keep the form from submitting.
		return false;
	},
	
	// Triggered when the AJAX request to post a new comment completes.
	// Only triggered in non-crossdomain setups.
	// Warning: runs outside of instance scope.
	postCommentRequestFinished: function(data) {
		var id = data.block_id;
		var inst = $ChirrupUIController.getInstanceForID(id);
		var root_context = inst.rootContext();
		// Reset the form state
		inst.commentFieldTouched = false;
		// Re-enable the form bits
		jQuery("fieldset.submit input", root_context).attr("disabled", "").val(inst.l("submit_label"));
		
		// What was the status of the attempt to post?
		if(data.result == 1) {
			// Success! Switch back to the message panel and show a confirmation for a short while
			// Reset the form value
			jQuery("#chirrup_"+id+"_field_comment", root_context).val(inst.l("post_default"))
			// Remove the 'no comments' item if it's present
			jQuery(".chirrup_no_comments", root_context).remove();
			// Show the list view
			inst.switchUI("comments");
			// Show an alert in the list view (clear the existing alert first)
			jQuery(".chirrup_notification_success", root_context).remove();
			inst.listContext().prepend(
					jQuery("<li>"+inst.l("notification_post_successful")+"</li>").attr({
						"class": "chirrup_notification_success"
					})
			);
		} else {
			// Epic fail. Place validations on the user and password fields to have the user check their details.
			inst.addValidationErrorToField(jQuery("#chirrup_"+id+"_field_username", root_context), inst.l("validation_bad_login"));
		}
	},
	
	addValidationErrorToField: function(field, message) {
		jQuery("<div class=\"chirrup_validation_error\">"+message+"</div>").insertBefore(field);
	},
	
	togglePreview: function() {
		var root_context = jQuery("#"+this.root_id);
		// Show and update the preview
		jQuery(".chirrup_preview", root_context).toggle();
		return false;
	},
	
	clearValidations: function() {
		this.validation_failures = false;
		var root_context = this.rootContext();
		jQuery(".chirrup_validation_error", root_context).remove();
		jQuery(".chirrup_errors", root_context).removeClass("chirrup_errors");
	},
	
	// When the message field is being typed into, the user is given a character countdown.
	drawCharCounter: function(remaining) {
		root_context = jQuery("#"+this.root_id);
		klass = (remaining < 1)? "chirrup_charcount_remaining chirrup_no_chars_remain" : "chirrup_charcount_remaining";
		jQuery(".chirrup_charcounter", root_context).html( 
			"<span class=\""+klass+"\">"+remaining+"</span> "+this.l("character_count_suffix")+" <a href=\"#"+this.root_id+"\" onclick=\"return $ChirrupUIController.getInstanceForID("+this.id+").togglePreview();\">"+this.l("toggle_preview")+"</a>"
		);		
	},
	
	// Formats a status text for display.
	processStatusText: function(text) {
		// Replace @names in the message text with links.
		return text.replace(/\@([^\s-]+)/g, "@<a rel=\"external\" href=\"http://twitter.com/$1\">$1</a>");
	},
	
	// Switches from comment to post tabs and back again
	switchUI: function(suffix) {
		alt_suffix = (suffix=="comments")? "form" : "comments";
		jQuery(".chirrup_"+suffix+".chirrup_inactive").removeClass("chirrup_inactive").addClass("chirrup_active");
		jQuery(".chirrup_"+alt_suffix+".chirrup_active").removeClass("chirrup_active").addClass("chirrup_inactive");
		return false;
	},
	
	// Returns an onclick attribute calling back to a given function
	onClickForMethod: function(method_name, args) {
		return "$ChirrupUIController.getInstanceForID("+this.id+")."+method_name+"("+args+"); return false;"
	},
	
	// Are comment elements .active or .inactive?
	activityClassForComments: function() {
		return ((this.options["startWith"]=="comments")? "chirrup_comments chirrup_active" : "chirrup_comments chirrup_inactive");
	},
	
	// Likewise for the form
	activityClassForForm: function() {
		return ((this.options["startWith"]=="form")? "chirrup_form chirrup_active" : "chirrup_form chirrup_inactive");
	},
	
	// Returns the template (the starting state of the textarea) for the current instance once
	// JSON data has been loaded from the server.
	tweetTemplateForData: function(data) {
		return "@"+data.chirrup_username+" "+data.tinyurl+" "+this.l("post_default");
	},
	
	// Strings	
	strings: {
		master: {
			loading_message: 											"Loading comments for this page...",
			navigation_comments: 									"Comments",
			navigation_form: 											"Comment with Twitter!",
			form_legend: 													"Post a comment using Twitter!",
			username_label: 											"Twitter username",
			password_label: 											"Password",
			comment_label: 												"Your comment",
			submit_label: 												"Comment!",
			submit_label_progress:								"Posting your comment...",
			no_comments: 													"No comments have been posted.",
			post_explanation: 										"Your username and password will not be saved. "+
																						"You can also comment by pasting the following into Twitter: ",
			toggle_preview: 											"Toggle preview",
			post_default: 												"I love this page!",
			character_count_suffix: 							"characters remaining.",
			notification_post_successful: 				"Thanks! Your comment was successfully posted and will be displayed here in the next minute or so.",
			validation_blank_username: 						"You must enter your Twitter username",
			validation_blank_password: 						"You must enter your Twitter password",
			validation_bad_comment_structure: 		"The comment must begin with @aName aURL",
			validation_comment_too_long: 					"The comment you entered is too long.",
			validation_no_comment_entered: 				"You should enter a comment before posting.",
			validation_bad_login: 								"Couldn't post your comment. Please check that you entered your Twitter username and password correctly.",
			chirrup_attribution: 									"Twitter comments by <a href=\"http://chirrup.angryamoeba.co.uk/\" class=\"angryamoeba\" rel=\"external\">Chirrup</a> from <a href=\"http://singlecell.angryamoeba.co.uk/\" class=\"angryamoeba\" rel=\"external\">Angry amoeba</a>"
		},
		en: {}
	},
	
	templates: {
		form: 				"<form><fieldset><legend /><dl /></fieldset><fieldset /></form>",
		//status: 			"<li><a><img /></a><blockquote><p><cite><a /></cite></p></blockquote></li>"
		status: 			"<li><a><img /></a><span><a class=\"tuser\" /> <cite></cite></span><p></p></li>"
	},
	
	// Accessors for common jQuery contexts and nodes used in methods
	rootContext: function() {
		return jQuery("#chirrup_"+this.id+"_root");
	},
	listContext: function() {
		return jQuery("#chirrup_"+this.id+"_list");
	},
	formContext: function() {
		return jQuery("#chirrup_"+this.id+"_form");
	},
		
	// Retrieve a localised string. If a string is not present in the dictionary specified
	// in options["language"], the master dictionary will be used.
	l: function(key) {
		local_dict = this.strings[this.options["language"]];
		master_dict = this.strings["master"];
		return (local_dict[key])? local_dict[key] : master_dict[key];
	},
	
	// Debug
	say: function(str) {
		if(this.consoleSpam) console.log(str);
	}
		
};

// Chirrup post count UI
// This class shows a small link with the number of comments for the given page, e.g.
//	<a class="chirrup_postcount" href="urltomyawesomepage"><span class="chirrup_postcount_quant">16</span> comments.</a>

// Inherit from: Chirrup UI


ChirrupPostCountUI = function(chirrup_url, page_url, data, options) {
	this.init(chirrup_url, page_url, data, options);
}
ChirrupPostCountUI.prototype = jQuery.extend({}, ChirrupUI.prototype, {
	
	build: function() {
		// Inserts a hook element containing a loading message. This hook element will be the container for the hook element loaded from the Chirrup service.
		document.write("<a id=\""+this.root_id+"\" class=\"chirrup chirrup_postcount chirrup_loading\">"+this.l("loading_message")+"</a>");		
		this.drawUIWithReceivedData(this.data);
	},
	
	drawUIWithReceivedData: function(data) {
		var root_id = "chirrup_"+this.id+"_root";
		var n = data.statuses.length;
				
		jQuery("#"+root_id).attr({
			"href": this.page_url,
			"class": "chirrup chirrup_postcount",
			"id": root_id
		}).html("<span class=\"chirrup_postcount_quant\">"+n+"</span> Comments");
	}
	
});

