// ==UserScript==
// @name    GetGMailPermaLink
// @namespace    mtamaki.com
// @include     https://mail.google.com/mail/*
// @include     http://mail.google.com/mail/*
// ==/UserScript==

(function() {
////////////////////////////////////////////////////////////////////////////////

function string_strip(string, strip_str) {
	strip_str = strip_str || '\\s'
	var re = new RegExp('^%(strip_str)*(.*?)%(strip_str)*$'.replace(/%\(strip_str\)/g, strip_str))
	return string.replace(re, "$1")
}

function xpath_get(xpath, node) {
	//ref: http://subtech.g.hatena.ne.jp/cho45/20071119/1195408940
	node = node || document;
	var result = (node.ownerDocument || node).createExpression(xpath,
		function(prefix) {
			var result = document.createNSResolver(node).lookupNamespaceURI(prefix);
			if(result)
				return result;
			else
				return (document.contentType == 'application/xhtml+xml') ? 'http://www.w3.org/1999/xhtml' : '';
		}
	).evaluate(node, XPathResult.ANY_TYPE, null);
	switch (result.resultType) {
		case XPathResult.STRING_TYPE : return result.stringValue;
		case XPathResult.NUMBER_TYPE : return result.numberValue;
		case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
		case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: {
			var results = [];
			var item;
			while(item=result.iterateNext(), item)
				results.push(item);
			return results;
		}
	}
	return null;
}

////////////////////////////////////////

function dom_get_parent_by_tag_name(element, tag_name) {
	while(element && tag_name != element.tagName)
		element = element.parentNode;
	return element
}

////////////////////////////////////////

function array_each(obj, func) {
	if('number' == typeof obj)
		for(var index = 0; index < obj; ++index) { if(func(index)) return }
	else if(undefined != obj.length)
		for(var index = 0; index < obj.length; ++index) {
			if(func(obj[index], index)) return
		}
	else
		for(var name in obj) { if(func(obj[name], name)) return }
}

////////////////////////////////////////

function array_each_result(array, init, func) {
	array_each(array, function(value, key) {
		result = func(init, value, key); init = result[0];
		if(1 < result.length) return result[1]; return false;
	})
	return init
}

////////////////////////////////////////

function array_reduce(array, init, func) {
	return array_each_result(array, init,
		function(result, value, key) { return [func(result,value, key)]; }
	)
}

////////////////////////////////////////

function array_filter(array, func) {
	return array_reduce(array, [],
		function(results, value, key) {
			var result = func(value, key);
			if(result)
				results.push(value);
			return results
		}
	)
}

////////////////////////////////////////

//funcが最初にtrueを返した要素を返す。見つからなかった場合はnull。
//findした結果を加工したいときは、コールバックの中ではなく、この関数の戻り値に対して処理する。
function array_find(array, func) {
	return array_each_result(array, null, function(result, item) { if(func(item)) return [item, true]; return [null] })
}

////////////////////////////////////////

//要素全てをfuncで処理して返す
function array_map(array, func) {
	return array_reduce(array, [],
		function(results, value, key) {
			results.push(func(value, key)); return results
		}
	)
}

////////////////////////////////////////

function dom_insert_before (target, element) {
	target.parentNode.insertBefore(element, target)
}

////////////////////////////////////////

function dom_create(name, attrs, childs, events) {
	//ref $N in fastlookupalc.user.js
	attrs = attrs || {}; events = events || {}; childs = childs || []
	var result = document.createElement(name);
	for(attr in attrs)
		result.setAttribute(attr, attrs[attr])
	for(var index=0; index < childs.length; ++index) {
		if('string' == typeof childs[index])
			result.appendChild(document.createTextNode(childs[index]))
		else
			result.appendChild(childs[index])
	}
	for(event in events)
		result.addEventListener(event, events[event], false)
	return result
}

////////////////////////////////////////

function event_observe(target_element, event_name, observer_func, capture_or_bubbling) {
	target_element.addEventListener(event_name, observer_func, capture_or_bubbling || false);
}

////////////////////////////////////////

function http_get(url, after_func, options) {
	var count = 0;
	if(!http_get.is_not_log) {
		if(!http_get.count)
			http_get.count = 0
		count = http_get.count++
		GM_log((++http_get.count) + ': ' + url)
	}
	options = options || {}
	options['url'] = url
	options['onload'] = function(details) {
		if(!http_get.is_not_log)
			GM_log(count + ': ' + url + ' ' + details.status)
		after_func(details.responseText, details)
	}
	options['headers'] = options['headers'] || {}
	if(options['data']) {
		options['method'] = 'POST'
		options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
	} else
		options['method'] = 'GET'
	GM_xmlhttpRequest(options)
}

////////////////////////////////////////

function dom_remove_all_children(element) {
	while(element.childNodes.length)
		element.removeChild(element.childNodes[0])
}

////////////////////////////////////////

function dom_remove(element) {
	element.parentNode.removeChild(element)
}

////////////////////////////////////////

function event_dispatch(target, event_type, type) {
	var event = document.createEvent(event_type);
	event.initMouseEvent(type, true, true, null, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
	target.dispatchEvent(event)
}

////////////////////////////////////////

function event_dispatch_click(target) {
	event_dispatch(target, 'MouseEvents', 'click')
}

////////////////////////////////////////

function gmail_ready(callback) {
	event_observe(window, 'load',
		function() {
			function gmail_wait() {
				if(
					array_find(
						document.body.getElementsByTagName('INPUT'),
						function(input){ return (input.hasAttribute('label')) }
					)
				) {
					if (unsafeWindow.gmonkey) {
						unsafeWindow.gmonkey.load('1.0', callback);
						return
					}
					function _rVCC_impl(){
						var last = gmail.getActiveViewElement()
						if(_rVCC_impl.last != last) {
							_rVCC_impl.last = last
							_rVCC_impl.callback()
						}
						setTimeout(_rVCC_impl, 1000);
					}
					var gmail = {
						registerViewChangeCallback: function(callback) {
							_rVCC_impl.callback = callback
						},
						getActiveViewType: function(){
							var spans = document.body.getElementsByTagName('SPAN');
							var details = array_filter(spans, function(span) {return('show details' == span.innerHTML)})
							if(details)
								return 'cv'
							return null
						},
						getActiveViewElement: function(){
							var spans = document.body.getElementsByTagName('SPAN');
							var details = array_filter(spans, function(span) {return('show details' == span.innerHTML)})
							if(details.length) {
								var element = dom_get_parent_by_tag_name(details[0], 'TABLE')
								if(!element)
									return null
								element = dom_get_parent_by_tag_name(element.parentNode, 'TABLE')
								if(!element)
									return null
								return element.parentNode
							}
							return null
						},
					}
					callback(gmail)
					_rVCC_impl()
					
				} else
					setTimeout(gmail_wait, 1000)
			}
			gmail_wait()
		}
	)
}

////////////////////////////////////////

function gmail_navigation_bar_add_link(gmail, name, callback) {
	gmail.registerViewChangeCallback(
		function () {
			if('cv' == gmail.getActiveViewType()) {
				//ビューのトップのdivを取る。
				var element = gmail.getActiveViewElement()
				if(!element)
					return
				//子が複数あるdivの最初のdivまでたどる。これがメールの上のナビゲーションバー
				while(1 == element.childNodes.length)
					element = element.childNodes[0]
				element = element.childNodes[0]
				//もう一度子が複数あるdivの最初のdivまでたどる。これがナビゲーションバーの左側
				while(1 == element.childNodes.length)
					element = element.childNodes[0]
				element = element.childNodes[0]
				//もう一度子が複数あるdivまでたどる。これがナビゲーションバーの左側の、実際に各要素が入ってる親div
				while(1 == element.childNodes.length)
					element = element.childNodes[0]
				//取れたelementの末尾にSpaceCharsFixのリンクを追加する
				element.appendChild(dom_create('div', {'class':element.firstChild.className},[name], {'click':
					function(event) {
						var processing = dom_create('div', {'class':element.firstChild.className},['GetPermalink Processing...'])
						dom_insert_before(event.target, processing)
						dom_remove(event.target)
						callback(event, function() { dom_remove(processing) })
					}
				}))
			}
		}
	);
}

////////////////////////////////////////

function gmail_get_form_data(key) {
	var match = array_find(document.body.childNodes, function(node) { return ('FORM'==node.tagName) }).getAttribute('action').match(new RegExp(key + '=([^&]+)'))
	if(match)
		return match[1]
	return null
}

////////////////////////////////////////

function gmail_get_ik(callback) {
	if(!gmail_get_ik.ik) {
		http_get(location.protocol + '//' + location.host + '/mail/',
			function(result) {
				//gmail_get_ik.ik = result.match(/ID_KEY:"([^"]+)"/)[1]; callback(gmail_get_ik.ik) 
				gmail_get_ik.ik = string_strip(result.match(/var GLOBALS=\[([^\]]+)\]/)[1].split(',')[9], '"')
				callback(gmail_get_ik.ik)
			}
		)
	} else
		callback(gmail_get_ik.ik)
}

////////////////////////////////////////

function gmail_get_current_mail_thread_id() {
	//基本的にURLの末尾がメールスレッドの先頭のメールID
	var th = window.parent.location.href.split('/')
	th = th[th.length-1]
	//たまにURLがルートのままのときがある。
	if(!th.match(/[0-9a-z]+/i)) {
		if(confirm("can't get mail thread id. reload?"))
			window.parent.location.href = location.protocol + '//mail.google.com'
	}
	return th
}

////////////////////////////////////////

function mail_parse(raw) {
	var splited = raw.replace(/\r/mg,'').split('\n\n')
	var last_matched = null
	var headers = array_reduce(splited[0].split('\n'), {},
		function(headers, header_line) {
			var matched = header_line.match(/^([a-zA-Z-]+): (.*)/)
			if(matched && 2 < matched.length) {
				last_matched = matched[1].toLowerCase()
				headers[last_matched] = matched[2]
			} else {
				headers[last_matched] += '\n' + header_line
			}
			return headers
		}
	)
	splited.shift()
	return [headers, splited.join('\n\n')]
}

////////////////////////////////////////

function gmail_get_original_raw(ik, msg, callback) {
	http_get(location.protocol + '//' + location.host + '/mail/?ui=2&ik=' + ik + '&view=om&th=' + msg,
		function(result) {
			//メールデータをパース
			callback(mail_parse(result))
		}
	)
}

////////////////////////////////////////

function gmail_get_elements_of_show_details(gmail) {
	var spans = gmail.getActiveViewElement().getElementsByTagName('SPAN');
	var details = array_filter(spans, function(span) {return('show details' == span.innerHTML)})
	return details
}

////////////////////////////////////////

function gmail_expand_all(gmail, thread_mail_count, callback) {
	var expand_all = array_find(gmail.getActiveViewElement().getElementsByTagName('U'), function(element) { return ('Expand all' == element.innerHTML) })
	if(expand_all) {
		//Expand allをクリック
		event_dispatch_click(expand_all);
		//全てのメールが開き終わるまで待つ
		(function wait_expand_all(callback) {
			setTimeout(
				function() {
					var details = gmail_get_elements_of_show_details(gmail)
					if(thread_mail_count <= details.length) {
						//show detailsも開く
						array_each(details, function(detail){event_dispatch_click(detail)})
						var spans = gmail.getActiveViewElement().getElementsByTagName('SPAN');
						var details = array_filter(spans, function(span) {return('hide details' == span.innerHTML)})
						callback(details)
					} else
						wait_expand_all(callback)
				},
				250
			)
		})(callback)
	} else {
		//show detailsも開く
		var details = gmail_get_elements_of_show_details(gmail)
		array_each(details, function(detail){event_dispatch_click(detail)})
		var spans = gmail.getActiveViewElement().getElementsByTagName('SPAN');
		var details = array_filter(spans, function(span) {return('hide details' == span.innerHTML)})
		callback(details)
	}
}

////////////////////////////////////////

function gmail_get_ids_of_mail_thread(current_mail_thread_id, ik, callback) {
	http_get('https://mail.google.com/mail/?ui=2&ik=' + ik + '&view=cv&th=' + current_mail_thread_id + '&rt=h&search=inbox',
		function(result) {
			callback(array_map(result.match(/D\(\["ms","[^"]+"/gmi), function(match) { return match.match(/D\(\["ms","([^"]+)"/)[1] }))
		}
	)
}

////////////////////////////////////////

function gmail_title_get(gmail) {
	//はじめにビューの要素を得る
	var element = gmail.getActiveViewElement()
	//子が複数あるdivの最初のdivまでたどる。これがメールの上のナビゲーションバー
	while(1 == element.childNodes.length)
		element = element.childNodes[0]
	element = element.childNodes[0]
	//その次の弟がメール本文のTABLE
	element = element.nextSibling
	//TABLEの子のうち一番初め出てくるH1がタイトル領域
	element = element.getElementsByTagName('H1')[0]
	//タイトル領域の最初の子SPANがタイトル
	return element.childNodes[0]
}

////////////////////////////////////////

function gmail_from_get(gmail) {
	
	var spans = gmail.getActiveViewElement().getElementsByTagName('SPAN');
	var from_span = array_filter(spans, function(span) {return('from' == span.innerHTML)})
	return from_span[0].parentNode.nextSibling.textContent
}

////////////////////////////////////////

function gmail_check_thread_id(gmail) {
	//ビューが切り替わったのにURLが変わってないときには正しくスレッドIDが取れないのでリロードを促す。
	var state = function() {}
	state = function() {
		//まず、最初のURLとビュータイプを保存。
		var view = gmail.getActiveViewType()
		var url = window.parent.location.href
		state = function() {
			//次に、ビュータイプが変わった時にURLが同一ならおかしい。
			if(view != gmail.getActiveViewType() && url == window.parent.location.href) {
				if(confirm("can't get thread id. reload?"))
					window.parent.location.href = location.protocol + '//mail.google.com'
			} else
				//変わってたらチェック終了
				state = function() {}
		}
	}
	gmail.registerViewChangeCallback(
		function () {
			state()
		}
	);
}

////////////////////////////////////////

function style_display_invert(element) {
	if('none' != element.style.display.toString())
		element.style.display = 'none'
	else
		element.style.display = ''
}

////////////////////////////////////////

function event_cancel(event) {
	if(event.preventDefault) {
		event.preventDefault();
		event.stopPropagation();
	} else {
		event.returnValue = false;
		event.cancelBubble = true;
	}
}

////////////////////////////////////////////////////////////////////////////////

gmail_ready(
	function(gmail) {
		gmail_check_thread_id(gmail)
		//ナビゲーションバーにGetPermalinkのリンクを追加
		gmail_navigation_bar_add_link(gmail, 'GetPermalink',
			function(event, done_func) {
				//GetPermalinkのリンクがクリックされると呼ばれる。
				//このメールスレッドのメール全てに[Permalink]リンクを加える。

				//セッションIDを取得
				gmail_get_ik(
					function(ik) {
						//このメールスレッドのメールのIDのリストを取得
						gmail_get_ids_of_mail_thread(gmail_get_current_mail_thread_id(), ik,
							function(ids) {
								if(!ids.length)
									return
								//取得できたらスレッド内のメールを全部展開
								gmail_expand_all(gmail, ids.length,
									function(details) {
										//展開したら各メールのshow detailsの隣に[Permalink]リンクを加える処理をする。

										//処理の終わったメール数のカウンタ
										var done = 0;
										//各メールごとに処理
										array_each(details.length,
											function(index) {
												//まずこのメールのオリジナルデータを取得
												gmail_get_original_raw(ik, ids[index],
													function(mail_data) {
														//オリジナルデータのMessage-Idを使って、[Permalink]タグを追加
														//Permalinkを作る
														var permalink_url = 'https://mail.google.com/mail/#search/' + encodeURIComponent(mail_data[0]['message-id'])
														//aタグを用意
														var permalink_a = dom_create('a', {'href':permalink_url, 'style':'margin-right:1ex'}, ['[Permalink]'])
														//clickイベントにアタッチして、クリックされたら[permalink subject (From: from, At: date)]というTracLinkを表示するようにする
														event_observe(permalink_a, 'click',
															function(event) {
																//イベントをキャンセルする
																event_cancel(event)
																//fromを解決。短くするために純粋なメアドだけ抜く
																//var from = mail_data[0]['from']
																var from = gmail_from_get(gmail)
																var matched = from.match(/<(.*)>/)
																if(matched)
																	from = matched[0]
																//dateを解決。
																var date = new Date()
																date.setTime(Date.parse(mail_data[0]['date']))
																function fix_figure(value) { if(value<10) return '0' + value; return value }
																date = fix_figure(date.getYear() % 100) + '/' + fix_figure(date.getMonth()+1) + '/' + fix_figure(date.getDate()) + ' ' + fix_figure(date.getHours()) + ':' + fix_figure(date.getMinutes())
																//テキストを書き換え
																prompt('Copy TracLink', '[' + permalink_url + ' ' + gmail_title_get(gmail).textContent.replace('[', '［').replace(']', '］') + ' (From: ' + from + ', At: ' + date + ')]')
															}
														)
														//ヘッダを閉じないようにする
														event_observe(permalink_a, 'mousedown', function(event) { event_cancel(event) })
														//aタグを追加
														dom_insert_before(details[index], permalink_a)
														//全てのメールに対して処理が終わったら、終了関数を呼び出し。
														if(details.length <= ++done)
															done_func();
													}
												)
											}
										)
									}
								)
							}
						)
					}
				)
			}
		)
	}
)

////////////////////////////////////////////////////////////////////////////////
})()
