// ==UserScript==
// @name		   Gmail2GCal
// @namespace	   mtamaki.com
// @description    Gmail2GCal
// @include 	   https://mail.google.com/mail/*
// @include 	   http://mail.google.com/mail/*
// @include 	   http://mtamaki.com/trac/mtamaki/wiki/Gmail2GCal*
// ==/UserScript==

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

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.parentElement;
	return element
}

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

//objの要素を一つづつ順に取り出してfuncに処理させる。funcが真を返すと処理を中断する。
function array_each( obj, func ){
	//数値：その数だけループ。引数はインデックス
	if( "number" == typeof obj ) { for( var index = 0; index < obj; ++index ) { if( func( index ) ) return } }
	//文字列：文字ごとにループ
	else if( "string" == typeof obj ) { for( var index = 0; index < obj.length; ++index ) { if( func( obj.substr( index, 1 ) ) ) return } }
	//e4x：プロパティの値ごとにループ
	else if( "xml" == typeof obj ) { for each( var element in obj ) { if( func( element ) ) return } }
	else if( obj ) {
		//配列：要素ごとにループ
		if( null != obj.length ) { for( var index = 0; index < obj.length; ++index ) { if( func( obj[index] ) ) return } }
		//オブジェクト：プロパティの名前ごとにループ
		else { for( var name in obj ) { if( func( name ) ) return } }
	}
}

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

//array_reduce( [0,1,2,3], function( result, item ){ return result + item; }, 0 ) => 0+1+2+3 = 6
function array_reduce( array, func, init ) {
	array_each(array, function( item ){ init = func( init, item ) } )
	return init
}

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

//funcがtrueを返すもののみ返す
function array_filter( array, func ) {
	return array_reduce(array, function(results, item){ if( func( item ) ) results.push(item); return results }, [] )
}

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

//配列を順に見ながら、値を返していく。コールバック関数では[戻り値本体, 列挙を中断するならtrue]を返す。
function array_each_result( array, init, func ) {
	array_each(array, function( item ){ result = func( init, item ); init = result[0]; if( 1 < result.length ) return result[1]; return false; } )
	return init
}

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

//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, item){ results.push(func( item )); return results }, [] )
}

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

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

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

//080315:var抜けとスペルミスを修正
function dom_create( name, attrs, children, events ) {
	//ref $N in fastlookupalc.user.js
	attrs = attrs || {}; events = events || {}; children = children || []
	var result = document.createElement( name );
	for( var attr in attrs )
		result.setAttribute( attr, attrs[attr] )
	for( var index = 0; index < children.length; ++index ) {
		if( "string" == typeof children[index] )
			result.appendChild( document.createTextNode( children[index] ) )
		else
			result.appendChild( children[index] )
	}
	for( var 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 );
}

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

//080315:すでにmethod, content-typeが定義されていたら上書きしないように修正
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( 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"] ) {
		if( !options["method"] )
			options["method"] = "POST"
		if( !options["headers"]["Content-Type"] )
			options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
	}
	else if( !options["method"] )
		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() {
			if (unsafeWindow.gmonkey)
				unsafeWindow.gmonkey.load('1.0', callback);
		}
	)
}

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

//080315:nameを使っていなかったので修正
function gmail_navigation_bar_add_link( gmail, name, callback ) {
	gmail.registerViewChangeCallback(
		function () {
			if( "cv" == gmail.getActiveViewType() ) {
				//ビューのトップのdivを取る。
				var element = gmail.getActiveViewElement()
				//子が複数ある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},[name + " 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 )  } )
	}
	else
		callback( gmail_get_ik.ik )
}

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

//現在表示されているメールスレッドの先頭のメールのIDを取得する
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 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 mail_parse( raw ) {
	var splited = raw.replace(/\r/mg,"").split("\n\n")
	var headers = {}
	array_each( splited[0].replace(/;\n/mg, ";").split("\n"),
		function( header ){
			header = header.split(": ")
			headers[header[0].toLowerCase()] = header[1]
		}
	)
	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 )
						callback( details )
					else
						wait_expand_all( callback )
				},
				250
			)
		})( callback )
	} else
		callback( gmail_get_elements_of_show_details( gmail ) )
}

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

function gmail_get_ids_of_mail_thread( current_mail_thread_id, ik, callback ) {
	http_get(location.protocol + "//" + location.host + "/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 date_to_w3cdtf( date ) {
	//Ref: http://www.kawa.net/works/js/jkl/share/jse-date-w3cdtf.js
	var values = ["FullYear", "Month", "Date", "Hours", "Minutes", "Seconds", "TimezoneOffset"]
	for( var index = 0; index < values.length; ++index )
		values[index] = date["get" + values[index]]()
	var MONTH = 1
	values[MONTH] += 1
	var tz_offset = values.pop()
	values.push( tz_offset / 60 )
	values.push( tz_offset % 60 )
	var TZ_HOUR = 6
	var tz_pm = ( values[TZ_HOUR] > 0 ) ? "-" : "+";
	if ( values[TZ_HOUR] < 0 )
		values[TZ_HOUR] *= -1;
	for( var index = 0; index < values.length; ++index )
		if( values[index] < 10 )
			values[index] = "0" + values[index]
	return values[0] + "-" + values[1] + "-" + values[2] + "T" + values[3] + ":" + values[4] + ":" + values[5] + tz_pm + values[6] + ":" + values[7];
}

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

//080315:Googleカレンダーにイベントを作成する
function gcal_add_event( title, content, where, start, end, auth, callback ) {
	http_get("http://www.google.com/calendar/feeds/default/private/full", callback,
		{
			"headers" : {"Authorization" : 'AuthSub token="' + auth + '"', "Content-Type": "application/atom+xml"},
			"method" : "POST",
			"data" : '<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gd="http://schemas.google.com/g/2005"><category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/g/2005#event"></category><title type="text">' + title + '</title><content type="text">' + content + '</content><gd:where valueString="' + where + '"></gd:where><gd:when startTime="' + start + '" endTime="' + end + '"></gd:when></entry>'
		}
	)
}

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

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

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

var token = GM_getValue("token")

//もし認証用ページに着たら、認証を処理する
if( 0 == location.href.indexOf("http://mtamaki.com/trac/mtamaki/wiki/Gmail2GCal") ) {
	var temp_token = location.href.split("?")[1].split("=")[1]
	http_get( "https://www.google.com/accounts/AuthSubSessionToken",
		function( result ) {
			GM_log( result )
			token = result.split("\n")[0].split("=")[1]
			GM_log( token )
			GM_setValue("token", token)
			alert("Auth successful. Go back to gmail.")
			location.href = "https://mail.google.com/mail/"
		},
		{"headers" : {"Authorization" : 'AuthSub token="' + temp_token + '"'} }
	)
	return
}

//gmailだった場合
gmail_ready(
	function( gmail ) {
		gmail_check_thread_id( gmail )
		//ナビゲーションバーにAdd2Calenderのリンクを追加
		gmail_navigation_bar_add_link( gmail, "Add2Calender",
			function( event, done_func ) {
				//Add2Calenderのリンクがクリックされると呼ばれる。

				//はじめに、GoogleAuthのアカウントがなければ作る
				if( !token ){
					alert("Not found auth token. Move to google auth.")
					window.parent.location.href = "https://www.google.com/accounts/AuthSubRequest?next=http%3A%2F%2Fmtamaki.com%2Ftrac%2Fmtamaki%2Fwiki%2FGmail2GCal&scope=http%3A%2F%2Fwww.google.com%2Fcalendar%2Ffeeds%2F&session=1"
					return done_func()
				}

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

										//処理の終わったメール数のカウンタ
										var done = 0;
										//各メールごとに処理
										array_each( details.length,
											function( index ) {
												//まずこのメールのオリジナルデータを取得
												gmail_get_original_raw( ik, ids[index],
													function( mail_data ) {
														//オリジナルデータのMessage-Idを使って、[Add2Calender]タグを追加
														var link = dom_create("span", {"style":"padding-right:1ex;color:rgb(132,170,255);text-decoration:underline;cursor:pointer"}, ["Add2Calender"])
														event_observe( link, "mousedown",
															function( event ){
																event_cancel( event )
																//ユーザーにパラメータを確認
																var now = new Date()
																var an_hour = new Date()
																an_hour.setTime(now.getTime() + 60 * 60 * 1000);
																var params = array_each_result( [["title", title], ["content", location.protocol + "//" + location.host + "/mail/#search/" + encodeURIComponent( mail_data[0]["message-id"])], ["where"], ["start", date_to_w3cdtf(now)], ["end", date_to_w3cdtf(an_hour)]], {},
																	function( results, args ) {
																		var name = args[0]
																		args[0] = "Please enter " + args[0]
																		results[name] = prompt.apply( window, args )
																		if( "string" != typeof results[name] )
																			return [false, true]
																		return [results, false]
																	}
																)
																//キャンセルされたらやめる
																if( !params )
																	return
																//カレンダーに追加
																gcal_add_event( params["title"], params["content"], params["where"], params["start"], params["end"], token, function(result, details){ if(201 == details.status) alert("Create successful.") } )
															},
															true
														)
														dom_insert_before( details[index], link )
														//全てのメールに対して処理が終わったら、終了関数を呼び出し。
														if( details.length <= ++done )
															done_func();
													}
												)
											}
										)
									}
								)
							}
						)
					}
				)
			}
		)
	}
)

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