﻿// V2C Sync2ch Script

//【登録場所】 全体
//【ラベル】 Sync2ch同期
//【内容】 sync2ch.comウェブサービスのAPIを利用し、タブ一覧とお気に入り一覧を
//	   サーバーのデーターと同期します。

//【コマンド1】${SCRIPT:A}  v2c_sync.txt 		(Sync2ch同期)
//【コマンド2】${SCRIPT:A}  v2c_sync.txt option  	(同期設定)
//【コマンド3】${SCRIPT:A}  v2c_sync.txt auto  	(自動同期 ON/OFF)
//【コマンド4】${SCRIPT:A}  v2c_sync.txt autoStart  	(起動時自動同期) //StartUpフォルダへ登録
//【コマンド5】${SCRIPT:A}  v2c_sync.txt readAll  	(全タブ既読化)
 
var CLIENT_NAME = "V2C Sync";
var CLIENT_VERSION = "1.5"; 
var CONF_FILENAME = "conf.txt"; //設定ファイル名
var CLIENT_ID_FILENAME = "client_id.txt"
var COUNT_DATA_FILENAME = "data.txt";
var POST_DATA_FILENAME = "posts.txt";
var AUTO_PROPERTY_NAME = "key_auto_session";
var MIN_SYNC_SPAN_MINUTES = 3;//自動同期間隔制限
var DEFAULT_SYNC_SPAN_MINUTES = 10;//デフォルト自動同期間隔
var DEBUG = false;
var SAVE_LOG = false;

var ARG_OPTION = 'option'; //設定画面起動
var ARG_UNREAD_ALL = 'readAll';//全タブ既読化
var ARG_TOGGLE_AUTO = 'auto'; //自動同期トグル
var ARG_AUTOSTART = 'autoStart'; //自動同期開始
var ARG_DEBUG = 'debug';
var ARG_LOG = "log";

function main() {
	DEBUG = hasArg( ARG_DEBUG );
	SAVE_LOG = hasArg( ARG_LOG );
	
	if (hasArg( ARG_UNREAD_ALL )) {
		markAllTabsAsRead();
		return;
	}

	if (hasArg( ARG_AUTOSTART )) {
		startAutoSync();
		return;
	}

	if (hasArg( ARG_TOGGLE_AUTO ) ) { 
		if (v2c.getProperty(AUTO_PROPERTY_NAME)) {
			v2c.removeProperty(AUTO_PROPERTY_NAME);
			v2c.println("自動同期ストップリクエスト");
			v2c.context.setStatusBarText("自動同期終了");
		} else {
			startAutoSync();
		}
		return;
	}

	if (hasArg( ARG_OPTION )) {
		startConfigure();
		return;
	}
	
	trySync(true);
}

function hasArg(arg) {
	var args = v2c.context.args;
	for (var i = 0; i < args.length; i++) {
		if (args[i] == arg) {
			return true;
		}
	}
	return false;
}

function debug(str) {
	if (DEBUG)
		v2c.println(str);
}

function startAutoSync() {
	//多重起動阻止用セッションキー
	var thisSessionKey = "SK"+Math.random();
	v2c.putProperty(AUTO_PROPERTY_NAME, thisSessionKey);

	for(;;) {
		var currentSession = v2c.getProperty(AUTO_PROPERTY_NAME);
		if (!currentSession || currentSession != thisSessionKey) {
			v2c.setStatus("自動同期終了");
			v2c.println("自動同期終了");
			return;
		}

		v2c.setStatus("自動同期開始");
		v2c.println("自動同期開始");

		var conf = getConfObject();

		try {
			sync(conf);
		}catch(e) {
			var message = "■エラー(自動同期中) " + e.lineNumber+ "行目: "+ e.toString(); 
			v2c.println(message);
			v2c.context.setStatusBarText(message);
		}

		var span = new Number(conf.span);
		if (isNaN(span) || MIN_SYNC_SPAN_MINUTES > span) 
			span = MIN_SYNC_SPAN_MINUTES;//最低3分
		java.lang.Thread.sleep(span*60000);//60秒*1000ms
	}
}

function markAllTabsAsRead() {
	var all = getAllTabThreads();
	for (var i = 0; i < all.length; i++) {
		var th = all[i];
		th.resetUnread();
		th.clearNewMark()
	}
}

function getAllTabThreads () {
	var threads = [];
	for(var colIndex = 0; colIndex < v2c.resPane.columnCount; colIndex++) {
		var column = v2c.resPane.columns[colIndex];

		for(var tabIndex = column.tabCount - 1; tabIndex >= 0; tabIndex--) {
			var th = column.threads[tabIndex];
			if (th != null) {
				threads.push(th);
			}
		}
	}
	return threads;
}

//設定ファイルを読み取り、同期を試みます。
function trySync(showOptionScreenIfFailed, conf) {
	if (conf == null)
		conf = getConfObject();

	if (conf && conf.id && conf.password) {
		sync(conf); //同期を開始
	} else if (showOptionScreenIfFailed) {
		startConfigure();
	}
}

function sync(conf) {
	var id  = conf.id;
	var pass = conf.password;

	localCountData.open();
	postData.open();
	threadRepository.init();

	//クライアント番号の読み込み
	var clientId = authClient(id,pass);
	if (clientId == null) {
		var message = "クライアント番号を取得出来ませんでした。";
		v2c.println(message);
		v2c.context.setStatusBarText(message);
		return;
	}

	//同期番号の読み込み
	var syncNumber = 0;
	var syncNumFile = v2c.getScriptDataFile("sync_number.txt");
	if (syncNumFile.exists()) {
		try {
			var str = v2c.readFile(syncNumFile);
			syncNumber = java.lang.Integer.parseInt(str);
			v2c.println("同期番号(送信):"+syncNumber);
		} catch(e) {
			v2c.println("有効な同期番号を読み取れません。同期番号を0で初期化します。");
		}
	}


	var url = new java.net.URL( (conf.https?"https":"http") +"://"+getHost()+"/api/sync2");

	var conn = url.openConnection();
	conn.setDoOutput(true);// POST
	var useCompression = true;
	if (useCompression)
		conn.setRequestProperty("Encoding", "gzip");
	conn.setRequestProperty("Accept-Encoding", "gzip");
	conn.setRequestProperty("Authorization",
			"Basic " + Base64.encode(id+":"+pass));
	conn.setRequestProperty("User-Agent", CLIENT_NAME +" "+ CLIENT_VERSION);

	var os = conn.getOutputStream();
	if (useCompression) 
		os = new java.util.zip.GZIPOutputStream(os);
	var writer = new java.io.OutputStreamWriter(os, "utf-8");
	var bw = new java.io.BufferedWriter(writer);

	// リクエストXMLの作成
	var xml = createRequestXml(syncNumber, clientId, conf);
	if (SAVE_LOG) saveSentXML(xml);
	debug(xml);
	

	bw.write(xml);
	bw.close();


	// レスポンスXML受信
	var inputStream = conn.getInputStream();

	var encHeader = conn.getHeaderField("Content-Encoding");
	if (encHeader && encHeader+"" == "gzip") {
		inputStream = new java.util.zip.GZIPInputStream(inputStream);
	}

	//DOM
	var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
	var builder = factory.newDocumentBuilder();
	var doc = builder.parse(inputStream);

	var result = doc.documentElement.attributes.getNamedItem("result").nodeValue;
	if (result == "invalid_client_id") {
		//クライアント番号不許可のため、削除して終了
		var clientIdFile = v2c.getScriptDataFile(CLIENT_ID_FILENAME);
		v2c.writeStringToFile(clientIdFile, "");
		var message = "クライアント番号不許可。同期を中断します。"; 
		v2c.println(message);
		v2c.context.setStatusBarText(message);
		return;
	}

	var recievedSyncNumber = doc.documentElement.attributes.getNamedItem("sync_number").nodeValue;
	if (recievedSyncNumber) {
		var message = "同期完了("+ recievedSyncNumber + ") ["+ new Date().toLocaleTimeString()+"]";
		//解析Main
		parseResponseDocument(conf, doc, function(){
			v2c.writeStringToFile(syncNumFile, recievedSyncNumber+"");

			v2c.println("同期番号(受信):"+recievedSyncNumber);
			v2c.println(message);
			v2c.context.setStatusBarText(message);
		});
	}

}

function saveSentXML(xml) {
	try {
		var logFile = v2c.getScriptDataFile("sent_xml_log.txt");
		var str = logFile.exists() ? v2c.readFile(logFile)+"" : "";
		if (str.length > 500000) {
			str = str.substring(0, 470000);
		}
		var dateStr = "\n["+ new Date().toLocaleTimeString()+"]";
		str = dateStr + "\n" + xml +"\n"+ str + "\n";
		v2c.writeStringToFile(logFile, str);
	} catch(e) {
		v2c.println("ログ保存エラー");
	}

}

//クライアント認証プロセス
function authClient(id, pass) {
	var clientIdFile = v2c.getScriptDataFile(CLIENT_ID_FILENAME);
	var clientId = null;
	if (clientIdFile.exists()) {
		try {
			var str = v2c.readFile(clientIdFile);
			clientId = java.lang.Integer.parseInt(str);
			debug("クライアントID:"+clientId);
			return clientId;
		} catch (e) {
			v2c.println("有効なクライアントIDを読み取れません。");
		}
	}

	debug("クライアント認証を開始");

	var host = getHost();
	var scheme = host == "sync2ch.com" ? "https://" : "http://";
	var url = new java.net.URL( scheme + host +"/api/auth2");

	var conn = url.openConnection();
	conn.setRequestProperty("Accept-Encoding", "gzip");
	conn.setRequestProperty("Authorization",
			"Basic " + Base64.encode(id+":"+pass));
	conn.setRequestProperty("User-Agent", CLIENT_NAME +" "+ CLIENT_VERSION);

	var inputStream = conn.getInputStream();
	clientId = conn.getHeaderField("Sync2ch-Client-ID");

	conn.disconnect();

	if (clientId != null) {
		v2c.writeStringToFile(clientIdFile, clientId+"");

		return clientId;
	}

	return null;
}

function getHost() {
	var args = v2c.context.args;
	for (var i = 0; i < args.length; i++) {
		var arg = args[i];
		if (arg.length() > 5 && arg.substring(0,5) == "host:") {
			return arg.substring(5);
		}
	}
	return "sync2ch.com";
}

function isSyncThread(th) { //同期できるスレッドかどうか
	return th && !th.local && !th.bbs.twitter;
}

//newMarkResIndexとviewResIndexの初期化のためタブを開いていく
function initAllTabThreads () {
	var first = true;
	for(var colIndex = 0; colIndex < v2c.resPane.columnCount; colIndex++) {
		var column = v2c.resPane.columns[colIndex];
		var selectedThread = column.selectedThread ; 

		for(var tabIndex = column.tabCount - 1; tabIndex >= 0; tabIndex--) {
			var th = column.threads[tabIndex];
			if (th != null) {
				if (0 == th.viewResIndex) {
					column.openThread(th, false, false, false);
					if (first == true) {
						java.lang.Thread.sleep(600);
						first = false;
					}
				}
			}
		}

		if (selectedThread) {
			column.openThread(selectedThread, false, false, false);
		}
	}
}



function createRequestXml(syncNumber, clientId, conf) {
	var os = escapeXML(System.getProperty("os.name"));
	var syncPosts = conf.post ? "sync_rl=\"post\"": "";
	var xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
	xml += "<sync2ch_request client_id=\""+clientId+"\" client_name=\""+CLIENT_NAME+"\" client_version=\""
		+CLIENT_VERSION+"\" sync_number=\""+syncNumber+"\" os=\""+os+"\" "+syncPosts+">\n";


	var entityIndex = 0;
	var idToThreads = {}; // id -> th
	var idToBoards = {};
	var urlToIds = {}; //url -> id


	var initFinished = "InitFinished";
	var shouldInit = "true" != v2c.getProperty(initFinished);
	if (shouldInit) {
		initAllTabThreads();
		v2c.putProperty(initFinished,"true");
	}

	//タブ一覧
	if (conf.tab) {
		var column  = getSyncTabColumn(conf);
		if (column) {
			var idList = [];
			for(var tabIndex = 0; tabIndex < column.tabCount; tabIndex++) {
				var th = column.getThread(tabIndex);


				if (isSyncThread(th)) {
					threadRepository.putIfMissing(th);
					var id = urlToIds[th.url];
					if (id == undefined) {
						id = urlToIds[th.url] = entityIndex;
						idToThreads[id] = th;
						entityIndex++;
					} 

					idList.push(id);
				}
			}
			xml += "<thread_group category=\"open\" id_list=\""+idList.join(',')+"\" />\n";
		}
	}

	// お気に入り一覧
	if (conf.fav) {
		var hasDirChild = function(item) {
			for(var i = 0; i < item.childCount; i++) {
				var child = item.getChild(i);
				if (!child.thread && !child.board && child.label) {
					return true;
				}
			}
			return false;
		};
		var addChilds = function(tagName, attrArray, folder) {
			var useIdList = !hasDirChild(folder);

			var innerXML = "";
			var idList = useIdList ? [] : null;

			for(var i = 0; i < folder.childCount; i++) {
				var child = folder.getChild(i);

				if (child.thread) {
					var th = child.thread;
					threadRepository.putIfMissing(th);

					if (isSyncThread(th)) {
						var id = urlToIds[th.url];
						if (id == undefined) {
							id = urlToIds[th.url] = entityIndex;
							idToThreads[entityIndex] = th;
							entityIndex++;
						}
						if (useIdList) {
							idList.push(id);
						} else {
							innerXML += "<th id=\""+ id +"\"/>\n";
						}
					}
				}  
				else if (child.board) {
					var id = urlToIds[child.board.url];
					if (id == undefined) {
						id = urlToIds[child.board.url] = entityIndex;
						idToBoards[entityIndex] = child.board;
						entityIndex++;
					}
					if (useIdList) {
						idList.push(id);
					} else {
						innerXML += "<bd id=\""+ id+"\"/>\n";
					}
				}
				else if (child.label) {
					innerXML += addChilds('dir', {'name':escapeXML(child.label)}, child);
				}
			}

			if (useIdList) {
				attrArray['id_list'] = idList.join(',');
				return makeElement(tagName, attrArray, null);
			} else {
				return makeElement(tagName, attrArray, innerXML);
			}
		};


		var targetFavTab = getFavTabForSync(conf);
		if (targetFavTab) {
			xml += addChilds("thread_group", {'category':'favorite'}, targetFavTab.root);
		}

	}

	xml += "<entities>\n";
	for(var id in idToThreads) {
		xml += threadToXml(conf, idToThreads[id], id);
	}
	for (var id in idToBoards) {
		var bd = idToBoards[id];
		var boardName = fixBoardUrl(bd.url+"");
		xml += "<bd id=\""+id +"\" title=\""+escapeXML(bd.name)+"\" "
			+ "url=\""+escapeXML(boardName)+"\" />";
	}
	xml += "</entities>\n";


	xml += "</sync2ch_request>";

	return xml;
}

function escapeXML(str) {
	str = str + '';
	return str.replace(/&/g, '&amp;')
		.replace(/</g, '&lt;')
		.replace(/>/g, '&gt;')
		.replace(/"/g, '&quot;');
}


function makeElement(tagName, attrArray, innerXML) {
	if (innerXML) {
		var xml = "<" + tagName + arrayToAttributes(attrArray) +">\n";
		xml += innerXML;
		xml += "</" + tagName + ">\n";
		return xml;
	} else {
		return "<" + tagName + arrayToAttributes(attrArray) +"/>\n";
	}
}

//配列からXMLの属性表現を生成
function arrayToAttributes(assocArray) {
	var result = "";
	for (var i in assocArray) {
		result += " " + i + "=\"" + assocArray[i] + "\"";
	}
	return result;
}

function threadToXml(conf, th, id) {
	var attrs = [];
	attrs['title'] = escapeXML(th.title);
	attrs['url'] = escapeXML(th.url);
	attrs['id'] = id;

	var resCount = th.localResCount > th.resCount ? th.localResCount : th.resCount;
	if (resCount > 1) {
		attrs['count'] = resCount;
		if (localCountData.getCount(th.url)) { //dat落ち対策
			if (localCountData.getCount(th.url) > resCount) {
				attrs['count'] = localCountData.getCount(th.url);
			}
		}

		if (th.unread) {
			//未読時には現在位置まで既読済みにする。
			if (th.viewResIndex > th.newMarkResIndex) {
				th.newMarkResIndex = th.viewResIndex + 1;
			}
			attrs['read'] = th.newMarkResIndex;
		} else {

			attrs['read'] = resCount;
		}

		attrs['now'] = 1 + th.viewResIndex;
	}

	if (conf.post) {
		var posts = [];
		for (var i = 0; i < th.postResIndex.length; i++) {
			var idx = 1+Number(th.postResIndex[i]);
			posts.push(idx);
		}
		var reservedPosts = postData.getPosts(th.url);
		if (reservedPosts) {
			for(var i = 0; i < reservedPosts.length; i++) {
				posts.push(reservedPosts[i]);
			}
		}

		if (posts.length > 0) {
			attrs['rl_post'] = posts.join(',');
			debug(attrs['rl_post']);
		}
	}

	return "\t<th"+ arrayToAttributes(attrs) + "/>\n";
}


//属性がある場合のみ、JavaScriptの文字列にして返す。
function attr(el, name) {
	if (!el || !name) return undefined;

	if (el.attributes.getNamedItem(name)) {
		return "" + el.attributes.getNamedItem(name).nodeValue;
	}

	return undefined;
}


function ThreadInfo () {
	//this.thread = null;
}

ThreadInfo.fromElement = function(node) {
	var info = new ThreadInfo();

	info.isBoard = node.nodeName+"" == "bd";
	info.url = attr(node, 'url');
	info.title = attr(node, 'title');
	info.count = attr(node, 'count');
	info.status = attr(node, 's');
	info.readPos = attr(node, 'read');
	info.readingPos = attr(node, 'now');
	info.id = attr(node, 'id');
	var posts = attr(node, "rl_post");
	if (posts != undefined) {
		info.posts = (posts == "") ? [] : posts.split(',');
	}

	return info;
}


var threadRepository = {
map : {}
      ,init : function () {
	      this.map = {};
      }
      ,put : function (url, th) {
	      this.map[url] = th;
      }
      ,get : function  (url) {
	      return this.map[url];
      }
      ,contains: function(url) {
	      return this.map[url] !== undefined;
      }
	,putIfMissing : function(th) {
		this.map[th.url] = th;
	}
};

//重複確認用テーブルと関数
var updateManager = { updateThreadUrls : {}
	, allThreads : {}
	, init : function() {
		this.updateThreadUrls = {};
		this.allThreads  = {};
	}
	, addThread : function(th, threadInfo) {
		if (threadInfo.status == 'n')  {
			return;
		}

		if (!this.allThreads[th.url]) {
			this.allThreads[th.url] = th;
			var localResCount = th.localResCount;

			if (threadInfo.readPos > 0 && th.newMarkResIndex != threadInfo.readPos) {
				th.newMarkResIndex = threadInfo.readPos;
				if (localResCount <= threadInfo.readPos) {
					localCountData.deleteCount(th.url);
					if (th.unread) { 
						th.clearNewMark();
					}
				}
			}

			if (localResCount < threadInfo.readPos
					|| localResCount < threadInfo.count) {
				//未取得レスが想定されるので更新リストへ追加。
				threadInfo.thread = th;
				this.updateThreadUrls[th.url] = threadInfo;
				return;
			} 

			this.applyPosts(th, threadInfo);

			var readingPos = Number(threadInfo.readingPos);
			if (readingPos > 0 && th.viewResIndex != readingPos - 1) {
				th.viewResIndex  = readingPos - 1;
			}
		}
	}

	, applyPosts : function (th, threadInfo) {
		if (threadInfo.posts) {
			var shouldAdd = threadInfo.status == 'a';

			var reservePosts = [];
			debug("posts = "+ threadInfo.title + ": " + threadInfo.posts.join(","));
			for(var i = 0; i < threadInfo.posts.length; i++) {
				var res = th.getRes(threadInfo.posts[i]-1);
				if (res && th.postResLabel) {
					res.setResLabel(th.postResLabel);
				} else {
					reservePosts.push(threadInfo.posts[i]);
				}
			}

			if (reservePosts.length > 0) {
				postData.setPosts(th.url, th.title, reservePosts);
			} else if (!shouldAdd) {
				postData.deletePosts(th.url);
			}

			if (!shouldAdd) {
				//消去
				var removeResList = [];
				for (var i = 0; i < th.postResIndex.length; i++) {
					var resNumber = 1 + Number(th.postResIndex[i]);
					if (threadInfo.posts.indexOf(''+resNumber) == -1) {
						var res = th.getRes(th.postResIndex[i]);
						if (res) {
							removeResList.push(res);
						}
					}
				}
				for (var i in removeResList) {
					removeResList[i].setResLabel(null);
				}
			}
		}

	}
	, updateThreads : function () {
		for(var url in this.updateThreadUrls) {
			var threadInfo = this.updateThreadUrls[url];
			var th = threadInfo.thread;

			if (th.unread) {
				localCountData.add(th.url, threadInfo.count);
			}

			var success = th.updateAndWait(); 
			if (success) {
				localCountData.deleteCount(th.url);//更新に成功したらカウント消去
				th.newMarkResIndex = threadInfo.readPos;
				if (th.localResCount <= threadInfo.readPos) {
					th.clearNewMark();
				}
			} 

			this.applyPosts(th, threadInfo);
			th.viewResIndex  = Number(threadInfo.readingPos) - 1;
		}
		
		localCountData.deleteMissingThreads(this.allThreads);
		localCountData.save();
		postData.save();
	
	}
};


//書き込みラベルを適切に設定できなかった時のためのデータ
var postData = {
	// "http://dummy.2ch.net/test/read.cgi/bar/132112438.dat" :
	// {
	//    "posts" : [313, 532]
	// }
         map : {}
	 ,open : function() {
		 var postFile = v2c.getScriptDataFile(POST_DATA_FILENAME);
		 if (postFile.exists()) {
			 try {
				 var dataText = v2c.readFile(postFile);
				 this.map = eval(dataText+"");
			 } catch (e) {
				 v2c.println(POST_DATA_FILENAME+ "読み込みエラー");
			 }
		 }

	 }
	 ,save : function() {
		 var dataFile = v2c.getScriptDataFile(POST_DATA_FILENAME);
		 var str = "";
		 for(var i in this.map) {
			 var entry = this.map[i];
			 if (entry["posts"].length > 0) {
				 var joined = entry["posts"].join();
				 //if (joined != '') {
					 str += '"' + i +'":{"title":"'+entry.title+'", "posts":['+joined+"]},\n\t";
				 //}
			 }
		 }
		 v2c.writeStringToFile(dataFile, "({\n\t"+str+"\n})");
	 }
	 
	 ,getPosts : function (url) {
		 return this.map[url] ?  this.map[url]["posts"] : null;
	 }
	 ,setPosts: function (url, title, posts) {
		 this.map[url] = {"title": title, "posts" : posts};
	 }
	 ,deletePosts : function (url, posts) {
		 delete this.map[url];
	 }

};

//Dat落ち対策のためのカウント数保持
var localCountData =  {
map : {}
      ,open : function () {
	      var dataFile = v2c.getScriptDataFile(COUNT_DATA_FILENAME);
	      if (dataFile.exists()) {
		      try {
			      var dataText = v2c.readFile(dataFile);
			      this.map = eval(dataText+"");
		      }catch(e) {
			      v2c.println(COUNT_DATA_FILENAME+ "読み込みエラー");
		      }
	      }
      }
      ,save : function () {
	      var dataFile = v2c.getScriptDataFile(COUNT_DATA_FILENAME);
	      var str = "";
	      for(var i in this.map) {
		      str += '"' + i +'":'+this.map[i]+",\n\t";
	      }
	      v2c.writeStringToFile(dataFile, "({\n\t"+str+"\n})");
      }
      ,add : function(url, count) {
	      this.map[url] = count;
      }
      ,getCount : function(url) {
	      return this.map[url];
      }
      ,deleteCount : function(url) {
	      delete this.map[url];
      }
      ,deleteMissingThreads : function(list) {
	      //listに無いエントリを削除する
	      for(var i in this.map) {
		      if (!list[i]) {
			      delete this.map[i];
		      }
	      }
      }
};

function parseResponseDocument(conf, doc, onParseFinished) {
	updateManager.init();

	var topLevelNodes = doc.documentElement.childNodes;
	var entities = {}; //id -> threadInfo
	for(var i = 0; i < topLevelNodes.length; i++) {
		var groupNode = topLevelNodes.item(i);
		if (!(groupNode instanceof org.w3c.dom.Element)) {
			continue;
		}
		var tagName = ''+groupNode.nodeName;

		if (tagName == "entities") {
			for (var j = 0; j < groupNode.childNodes.length ; j++) {
				var node = groupNode.childNodes.item(j);
				if (!(node instanceof org.w3c.dom.Element) ) {
					continue;
				}

				var tagName = node.nodeName+"";

				var threadInfo = ThreadInfo.fromElement(node) ;
				entities[threadInfo.id] = threadInfo;

				if (threadInfo.isBoard == false) {
					var th = null;
					var url = threadInfo.url + '';
					if (threadRepository.contains(url)) {
						th = threadRepository.get(url);
					} else {
						th = v2c.getThread(url, threadInfo.title);
						if (th) {
							debug(url+","+ threadInfo.title);
							threadRepository.putIfMissing( th);
						}
					}
					if (th) {
						updateManager.addThread(th, threadInfo);
					}
				}
			}

		} else if (tagName == "thread_group") {
			var status = attr(groupNode, 's');
			if (status == "n") continue;


			var category = ""+groupNode.attributes.getNamedItem('category').nodeValue;

			if (category == "open") { //タブ一覧
				parseTabGroupElement(conf, groupNode, entities);
			} else if (category == "favorite") { //お気に入り一覧
				var targetFavoriteTab = getFavTabForSync(conf);

				if (targetFavoriteTab) {
					allThreads = getAllThreads(targetFavoriteTab, targetFavoriteTab.root);
					insertFavRecurse(targetFavoriteTab, groupNode, targetFavoriteTab.root, allThreads, entities);
				}
			}
		}

	}

	if (onParseFinished) {
		onParseFinished(); //時間がかかる更新の前に同期番号を保存しておく。
	}

	updateManager.updateThreads();
}



function parseTabGroupElement(conf, groupNode, entities) {
	var column = getSyncTabColumn(conf);
	if (!column) return;


	var skippedThreads = {};
	var allThreads = {};
	for(var tabIndex = column.tabCount - 1; tabIndex >= 0; tabIndex--) {
		var th = column.threads[tabIndex];
		if (th) {
			allThreads[th.url+""] = th;
		}
	}

	var idListAttr = attr(groupNode, 'id_list');
	var idList = null;
	if (idListAttr != undefined) {
		idList = idListAttr.split(',');
	} else {
		idList = [];
		for (var j = 0; j < groupNode.childNodes.length ; j++) {
			var node = groupNode.childNodes.item(j);
			if (!(node instanceof org.w3c.dom.Element) || node.nodeName+"" != "th") {
				continue;
			}

			var idAttr = attr(node, 'id');
			if (idAttr == undefined || !entities[idAttr]) 
				continue;

			idList.push(idAttr);
		}

	}
	var insertIndex = 0;
	if (idList != null)
	for (var j = 0; j < idList.length ; j++) {
		var idAttr = idList[j];
		if (idAttr == undefined || !entities[idAttr]) 
			continue;

		var threadInfo = entities[idAttr] ;

		//挿入可能位置まで進める
		insertIndex = proceedInsertIndex(column, insertIndex);

		var sameThread = false;
		var th = allThreads[threadInfo.url];

		if (threadRepository.contains(threadInfo.url)) {
			th = threadRepository.get(threadInfo.url);
		} else if (th == null) {
			th = v2c.getThread(threadInfo.url, threadInfo.title);
		}
		if (th== null) {
			continue;
		}

		if (insertIndex < column.tabCount) {
			var existTh = column.threads[insertIndex];
			if (existTh.url+'' == threadInfo.url) { //一致
				sameThread = true;
			} 
		}

		if (!sameThread) {
			if (th.columnIndex == -1) {
				column.openThread(th, false, true, true);
			}

			th.movePanelTo(conf.tabColumn, insertIndex >= column.tabCount ? -1 : insertIndex);
		}

		insertIndex++;
	}

	for(var k  = column.tabCount - 1 ; k >= insertIndex; k--) {
		var th = column.threads[k];
		if (isSyncThread(th))
			column.threads[k].close();
	}
	

}

function proceedInsertIndex(column, insertIndex) {
	for(var tabIndex = insertIndex; tabIndex < column.tabCount; tabIndex++) {
		var th = column.threads[tabIndex];
		if (isSyncThread(th)) {
			return tabIndex;
		}
	}
	return column.tabCount;
}

/**
 * お気に入りには複数のスレッドを登録できないので、
 * 削除時のため、全てのスレッドを参照しておく。
 */
function getAllThreads(favTab, folder) {
	var threads = {};
	for (var c = 0; c < folder.childCount; c++) {
		var child = folder.getChild(c);
		if (child.thread) {
			threads[child.thread.url+""] = child;
		}else if (child.board) {
			threads[child.board.url+""] = child;
		} else if (child.childCount > 0) {
			var childThreads = getAllThreads(favTab, child);
			for(var url in childThreads) {
				threads[url] = childThreads[url];
			}
		}
	}

	return threads;
}

function fixBoardUrl(boardName) {
	return boardName.replace("shitaraba.com", "livedoor.jp");
}

/**
 * お気に入りを受け取ったXMLの順番で格納していく再帰関数
 * フォルダはinsertItem関数を使うと閉じてしまうため対策が必要。
 */
function insertFavRecurse(favTab, groupNode, folder, removedAllThreads, entities) {
	var folders = {};
	var threads = {};
	var deleteFolders = {};
	var deleteThreads = {};
	var deleteBoards = {};


	//ハッシュマップの生成  url->thread, label->folder
	//と消去アイテムの削除
	if (folder.childCount > 0) {
		for (var c = folder.childCount-1; c >= 0; c--) {
			var child = folder.getChild(c);
			if (child) { 
				if (child.thread){
					threads[child.thread.url+""] = child.thread;
					deleteThreads[child.thread.url+""] = child.thread;
				} else if (child.board) {
					threads[child.board.url+""] = child.board;
					deleteBoards[fixBoardUrl(child.board.url+"")] = child.board;
				} else if (child.label){ //フォルダ
					folders[child.label+""] = child;
					deleteFolders[child.label+""] = child;
				}
			}
		}
	}


	var targetList = [];//groupNode.childNodes;

	//削除
	var idListAttr = attr(groupNode, 'id_list');
	var idList = null;
	if (idListAttr != undefined) {
		var idList = idListAttr.split(',');
		for (var i in idList) {
			var id = idList[i];
			var entity = entities[id];
			if (entity) {
				var url = entity.url;
				if (entity.isBoard) {
					targetList.push({tagName: "bd", "id":id});
					delete deleteBoards[url];
				} else {
					targetList.push({tagName: "th", "id":id});
					delete deleteThreads[url];
				}
			}
		}

	} else {
		for (var j=0; j < groupNode.childNodes.length ; j++) {
			var node = groupNode.childNodes.item(j);

			if (!(node instanceof org.w3c.dom.Element)) {
				continue;
			}


			if (node.nodeName+"" == "dir") {
				var name = '' + node.attributes.getNamedItem("name").nodeValue;
				targetList.push({tagName:'dir', "name": name, element: node});
				delete deleteFolders[name];
			} else if (node.nodeName+"" == "th") {
				var idAttr = attr(node, "id");
				var url = entities[idAttr].url;
				delete deleteThreads[url];
				targetList.push({tagName:'th', "id": idAttr});
			} else if (node.nodeName+"" == "bd") {
				var idAttr = attr(node, "id");
				var url = entities[idAttr].url;//attr(node, "url");
				targetList.push({tagName:'bd', "id": idAttr});
				delete deleteBoards[url];
			}
		}
	}
	for(var i in deleteBoards) {
		favTab.removeItem(deleteBoards[i]);
		delete threads[i];
	}
	for(var i in deleteThreads) {
		var th = deleteThreads[i];
		if (isSyncThread(th)) {
			favTab.removeItem(th);
			delete threads[i];
		}
	}
	for(var i in deleteFolders) {
		favTab.removeItem(deleteFolders[i]);
		delete folders[i];
	}


	//挿入と並び替え
	var insertIndex = 0;
	for (var j=0; j < targetList.length ; j++) {
		var node = targetList[j];


		var currentChild = null;
		if (insertIndex < folder.childCount) {
			currentChild = folder.getChild(insertIndex);
		}

		if (node.tagName+"" == "dir") {
			var name = node.name;
			var childFolder = null;

			if (currentChild && currentChild.label+"" == name  && !currentChild.thread && !currentChild.board) {
				childFolder  = currentChild; //現並びと一致
			} else if (folders[name]) {
				favTab.insertItem(folder, folders[name], insertIndex);
				childFolder = folders[name];
			}  else {
				//新規フォルダ
				childFolder = favTab.insertFolder(folder, name, insertIndex);
			}
			insertIndex++;

			if (childFolder && attr(node.element, 's') != "n") {
				insertFavRecurse(favTab, node.element, childFolder, removedAllThreads, entities);
			}

		} else if (node.tagName+"" == "th") {
			var idAttr = node.id;
			if (idAttr == undefined || !entities[idAttr]) {
				continue;
			}

			var threadInfo = entities[idAttr];

			var url = threadInfo.url;

			if (currentChild && currentChild.thread && currentChild.thread.url+"" == url) {
				delete threads[url];

			} else {
				if (removedAllThreads[url]) {
					favTab.removeItem(removedAllThreads[url]);
					delete removedAllThreads[url];
				}

				var th = null;
				if (threads[url]) {
					th = threads[url];
					favTab.removeItem(threads[url]);
				} else {
					th = v2c.getThread(url, threadInfo.title);
				}
				if (th) {
					favTab.insertItem(folder, th, insertIndex);
				}
			}
			insertIndex++;
		} else if (node.name+"" == "bd") {
			var idAttr = node.id;

			if (idAttr == undefined || !entities[idAttr]) {
				continue;
			}

			var boardInfo = entities[idAttr];

			var url = boardInfo.url;
			if (currentChild && currentChild.board && currentChild.board.url+"" == url) {
				delete threads[url];
			} else {
				if (removedAllThreads[url]) {
					favTab.removeItem(removedAllThreads[url]);
					delete removedAllThreads[url];
				}

				var bd = null;
				if (threads[url]) {
					bd = threads[url];
					favTab.removeItem(threads[url]);
				} else {
					bd = v2c.getBoard(url);
				}
				if (bd) {
					favTab.insertItem(folder, bd, insertIndex);
				}
			}
			insertIndex++;
		}
	}

}


//設定画面を起動
function startConfigure() {
	v2c.context.setPopupHTML(generateConfHTML());
	v2c.context.setTrapFormSubmission(true);
	v2c.context.setPopupFocusable(true);
}

function generateConfHTML() {
	var conf = getConfObject();
	var noConf = conf == null; //初期起動時にはこの変数を用いてデフォルト値を設定する。
	
	var id = 	noConf ? "" : conf.id;
	var pass =	noConf ? "" : conf.password;
	var tab =	noConf ? true : (conf.tab ? true : false);
	var fav  = 	noConf ? true : (conf.fav ? true : false);
	var post =	noConf ? false : (conf.post ? true : false);
	var https =	noConf ? false: (conf.https ? true : false);
	var favTab =	noConf ? -1 : conf.favTab;
	var tabColumn = noConf ? -1 : conf.tabColumn;
	var span = (noConf || isNaN(conf.span)) ? DEFAULT_SYNC_SPAN_MINUTES : conf.span;

	var html = '<html><head><style>td{font-size:10px; margin-top:1px; padding-top:0px;padding-bottom:0px} td.label{text-align:right}</style></head>';
	html += '<body style="margin:7px">';
	html += '<center><h1 style="font-size:19px; font-family:Arial">Sync2ch同期設定</h1>';
	html += '<div>'+CLIENT_NAME+ ' ' +CLIENT_VERSION+' (<a href="http://sync2ch.com/?c=v">http://sync2ch.com</a>)</div>';
	html += '<form action="GET">';


	html += '<table style="margin-top:4px; margin-bottom:4px">';

	html += '<tr>';
	html += '<td class="label">ID : </td>';
	html += '<td><input size="15" type="text" value="'+id+'" name="id"/></td>';
	html += '</tr>';

	html += '<tr>';
	html += '<td class="label">API接続用パスワード : </td>';
	html += '<td><input size="15" disabled type="password" value="'+pass+'" name="password"/></td>';
	html += '</tr>';

	html += '<tr>';
	html += '<td class="label">タブ一覧を同期する : </td>';
	html += '<td><input type="checkbox" '+ (tab ? "checked":"" )+' name="tab"/></td>';
	html += '</tr>';

	html += '<tr>';
	html += '<td class="label">お気に入りを同期する : </td>';
	html += '<td><input type="checkbox" ' + (fav ? "checked" : "")+' name="fav"/></td>';
	html += '</tr>';

	html += '<tr>';
	html += '<td class="label">書き込んだレス番号を同期する : <br/>(プレミアムアカウントのみ) &nbsp; </td>';
	html += '<td><input type="checkbox" ' + (post ? "checked" : "")+' name="post"/></td>';
	html += '</tr>';

	html += '<tr>';
	html += '<td class="label">SSL通信を利用する : </td>';
	html += '<td><input type="checkbox" ' + (https ? "checked" : "")+' name="https"/></td>';
	html += '</tr>';

	//html += '<tr ><td colspan="2"><hr/></td></tr>';

	html += '<tr>';
	html += '<td class="label">同期用タブカラム : </td>';
	html += '<td><select name="tabColumn">';

	for(var colIndex = 0; colIndex < v2c.resPane.columnCount; colIndex++) {
		var column = v2c.resPane.columns[colIndex];
		var shortText = (1+colIndex)+': ';
		for(var i in column.threads) {
			shortText += column.threads[i].title;
			if (shortText.length > 16) {
				shortText = shortText.substring(0, 16)+"..." ;
			}
			if (column.tabCount > 1) {
				shortText += "(他"+ (column.tabCount-1)+")";
			}
			break;
		}

		var selectAttr = colIndex == tabColumn ? "selected" : "";

		html += '<option '+selectAttr+' value="'+colIndex+'">'+shortText+'</option>';
	}
	html += '</select></td>';
	html += '</tr>';

	
	html += '<tr>';
	html += '<td class="label">同期用お気に入りタブ : </td>';
	html += '<td><select name="favTab">';
	for (var k = 0; k < v2c.favorites.count ; k++) {
		var fav = v2c.favorites.getFavorite(k);
		var selectAttr = (k == favTab) ? "selected": "";
		html += '<option value="'+k+'" '+selectAttr+'>'+(k+1)+": "+fav.name+'</option>';
	}
	html += '</select></td>';
	html += '</tr>';

	//html += '<tr ><td colspan="2"><hr/></td></tr>';

	html += '<tr>';
	html += '<td class="label">自動同期間隔(分) : </td>';
	html += '<td><input size="15" type="text" value="'+span+'" name="span"/></td>';
	html += '</tr>';

	html += '</table>';


	html += '<br/><div><input type="submit" value="適用"/>';
	html += '&nbsp;&nbsp;<input type="submit" value="キャンセル" name="cancel"/>';
	html += '</div>';
	html += '</form>';

	html += '</body>';
	html += '</html>';
	return html;
}


function formSubmitted(u,sm,sd) {
	var prefs = sd.split('&');

	var confText = "";
	var isCanceled = false;

	var id = null;
	var pass = null;

	for(var i in prefs) {
		var nameAndValue = prefs[i].split('=');
		var name = nameAndValue[0];
		var value = nameAndValue[1];

		if (name == 'cancel') {
			isCanceled = true;
			break;
		} 

		if (name == 'id') {
			id = value;
			value = e(value);
		} else if (name == 'password'){
			pass = value;
			value = e(value);
		} else {
			value = "\""+value+"\"";
		}

		confText += name+':'+value+",\n\t";
		debug(confText);

	}

	if (!isCanceled) {
		var idFile = v2c.getScriptDataFile(CONF_FILENAME);
		v2c.writeStringToFile(idFile, "({\n\t"+confText+"\n})");
	}

	v2c.context.closeOriginalPopup();

	//同期再開
	if (!isCanceled && !hasArg(ARG_OPTION) && id && pass) {
		trySync(false);
	}
}

//設定オブジェクトを取得
function getConfObject() {
	var idFile = v2c.getScriptDataFile(CONF_FILENAME);
	if (idFile.exists()) {
		var confText = v2c.readFile(idFile);
		var conf = eval(confText+"");
		if (conf) {
			conf.password = d(conf.password);
			conf.id = d(conf.id);
			return conf;
		}
	}
	return null;
}

function getFavTabForSync(conf) {
	var targetFavTab = v2c.favorites.getFavorite(conf.favTab);
	if (!targetFavTab) {
		for (var i = 0; i < v2c.favorites.count ; i++) {
			targetFavTab = v2c.favorites.getFavorite(i);
			break;
		}
	}

	return targetFavTab;
}

function getSyncTabColumn(conf) {
	var column  = null;
	var columnCount = v2c.resPane.columnCount;

	if (conf.tabColumn < columnCount) {
		column = v2c.resPane.columns[conf.tabColumn];
	} else if (columnCount > 0) {
		conf.tabColumn = 0;
		column = v2c.resPane.columns[0];
	}

	return column;
}

function e(value) {
	var sksSpec = new SecretKeySpec(key().getBytes(), "Blowfish");
	var cipher = Cipher.getInstance("Blowfish");
	cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sksSpec);
		
	var fin  = cipher.doFinal(new java.lang.String(value).getBytes());
	return "[" + fin.join(',') + "]";
}

function key() {
	var p = "I3";
	var ver = System.getProperty("os.version")+"";
	for(var i = 0; i <  ver.length && i < 3; i++) p+=ver[i];
	var ver = System.getProperty("user.name")+"";
	for(var i = 0; i <  ver.length && i < 4; i++) p+=ver[i];

	return new java.lang.String(p+"c");
}

function d(value) {
	try {
		var encrypted = new Packages.java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, value.length);
		for(var i = 0; i < value.length; i++) {
			encrypted[i] = value[i];
		}
		var sksSpec = new SecretKeySpec(key().getBytes(), "Blowfish");

		var cipher = Cipher.getInstance("Blowfish");
		cipher.init(Cipher.DECRYPT_MODE, sksSpec);

		return ""+new java.lang.String(cipher.doFinal(encrypted)); 
	} catch(e) {
		return "";
	}
	
}



/**
 *
 *  Base64 encode / decode
 *  http://www.webtoolkit.info/
 *
 **/

var Base64 = {

	// private property
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

	  // public method for encoding
	  encode : function (input) {
		  var output = "";
		  var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		  var i = 0;

		  input = Base64._utf8_encode(input);

		  while (i < input.length) {

			  chr1 = input.charCodeAt(i++);
			  chr2 = input.charCodeAt(i++);
			  chr3 = input.charCodeAt(i++);

			  enc1 = chr1 >> 2;
			  enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			  enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			  enc4 = chr3 & 63;

			  if (isNaN(chr2)) {
				  enc3 = enc4 = 64;
			  } else if (isNaN(chr3)) {
				  enc4 = 64;
			  }

			  output = output +
				  this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
				  this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);

		  }

		  return output;
	  },

	  // public method for decoding
decode : function (input) {
		 var output = "";
		 var chr1, chr2, chr3;
		 var enc1, enc2, enc3, enc4;
		 var i = 0;

		 input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

		 while (i < input.length) {

			 enc1 = this._keyStr.indexOf(input.charAt(i++));
			 enc2 = this._keyStr.indexOf(input.charAt(i++));
			 enc3 = this._keyStr.indexOf(input.charAt(i++));
			 enc4 = this._keyStr.indexOf(input.charAt(i++));

			 chr1 = (enc1 << 2) | (enc2 >> 4);
			 chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			 chr3 = ((enc3 & 3) << 6) | enc4;

			 output = output + String.fromCharCode(chr1);

			 if (enc3 != 64) {
				 output = output + String.fromCharCode(chr2);
			 }
			 if (enc4 != 64) {
				 output = output + String.fromCharCode(chr3);
			 }

		 }

		 output = Base64._utf8_decode(output);

		 return output;

	 },

	 // private method for UTF-8 encoding
_utf8_encode : function (string) {
		       string = string.replace(/\r\n/g,"\n");
		       var utftext = "";

		       for (var n = 0; n < string.length; n++) {

			       var c = string.charCodeAt(n);

			       if (c < 128) {
				       utftext += String.fromCharCode(c);
			       }
			       else if((c > 127) && (c < 2048)) {
				       utftext += String.fromCharCode((c >> 6) | 192);
				       utftext += String.fromCharCode((c & 63) | 128);
			       }
			       else {
				       utftext += String.fromCharCode((c >> 12) | 224);
				       utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				       utftext += String.fromCharCode((c & 63) | 128);
			       }

		       }

		       return utftext;
	       },

	       // private method for UTF-8 decoding
_utf8_decode : function (utftext) {
		       var string = "";
		       var i = 0;
		       var c = c1 = c2 = 0;

		       while ( i < utftext.length ) {

			       c = utftext.charCodeAt(i);

			       if (c < 128) {
				       string += String.fromCharCode(c);
				       i++;
			       }
			       else if((c > 191) && (c < 224)) {
				       c2 = utftext.charCodeAt(i+1);
				       string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				       i += 2;
			       }
			       else {
				       c2 = utftext.charCodeAt(i+1);
				       c3 = utftext.charCodeAt(i+2);
				       string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				       i += 3;
			       }

		       }

		       return string;
	       }

}

importPackage(java.lang);
importPackage(javax.crypto);
importPackage(javax.crypto.spec);

try {
	main();
} catch(e) {
	var message = "■エラー " + e.lineNumber+ "行目: "+ e.toString(); 
	v2c.println(message);
	v2c.context.setStatusBarText(message);
}
