// Nov 13, 2015 Koji Ota
// May 01, 2017 Koji Ota

////////////////////////////////////////
// load library
var FS = require("fs");
var PATH = require("path");
var URL =  require("url");

////////////////////////////////////////
// load configuration
var site = require("./site");

////////////////////////////////////////
// configuration
var config = {
	prog: {
		name: "ko2ib",
		version: "0.0.2",
		description: "",
		copyright: "Copyright (C) 2017 Koji Ota All right reserved.",
	},
	debug: {
		warning: 1,
		trace: 1,
		verbose: 1,
	},
	path: {
		database: "./public/data/database",
		urlbase: "/data/database",
	},
	env: {
		app: null,
		io: null,
		uploader: null,
	}
};

////////////////////////////////////////
// output
function color_output(color, msg) {
	var cin;
	var cout = '\u001b[0m';
	switch(color) {
		case "black":	cin = '\u001b[30m'; break;
		case "red":		cin = '\u001b[31m'; break;
		case "green":	cin = '\u001b[32m'; break;
		case "yellow":	cin = '\u001b[33m'; break;
		case "blue":	cin = '\u001b[34m'; break;
		case "magenta":	cin = '\u001b[35m'; break;
		case "cyan":	cin = '\u001b[36m'; break;
		case "white":	cin = '\u001b[37m'; break;
	}
	console.log(cin+msg+cout);
}

// 致命的なエラーに(処理は中止)
function panic(msg) {
	color_output("red", msg);
}
// 回復可能なエラーに(処理は中止)
function perror(msg) {
	color_output("magenta", msg);
}
// 値の間違い等警告に(処理は続行)
function pwarning(msg) {
	if (config.debug.warning) {
		color_output("yellow", msg);
	}
}
// 関数の入り口
function ptrace(msg) {
	if (config.debug.trace) {
		color_output("green", msg);
	}
}
// 関数内動作情報
function pverbose(msg) {
	if (config.debug.verbose) {
		color_output("cyan", msg);
	}
}
// 表示
function print(msg) {
	console.log(msg);
}

////////////////////////////////////////
// utility
// uuidを生成する
function uuid() {
	var S4 = function() {
		return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
	}
	return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4() +S4());
}

// オブジェクトが空かどうか判断する
function isEmpty(obj) {
	for(var k in obj) return false;
	return true;
}

// オブジェクトのダンプ
function objdump(obj) {
	if (obj == null) {
		pwarning("object is null.");
	}

	var nest = 0;
	if (arguments.length == 2) {
		nest = arguments[1];
	}

	var blanks = "";
	for(var i=0; i<nest; i++) {
		blanks += " ";
	}

	for(var k in obj) {
		pverbose(blanks+k+"="+obj[k]+", <"+typeof(obj[k])+">");
		if (typeof(obj[k]) == "object") {
			objdump(obj[k], nest+2);
		}
	}
}

////////////////////////////////////////
//
function Exception(str) {
	perror(str);
	console.trace();
	throw "error";
}


////////////////////////////////////////
// FileUtil
// statは以下の値を返す
// dev, ino, mode, nlink, uid, gid, rdev, size, blksize, blocks, atimeMs, mtimeMs, ctimeMS, birthtimeMs, atime, mtime, ctime, birthtime
// https://nodejs.org/api/fs.html
var FSUTL = {
	exists: function(f) {
		return FS.existsSync(f);
	},
	isFile: function(f) {
		return FS.statSync(f).isFile();
	},
	isDirectory: function(f) {
		return FS.statSync(f).isDirectory();
	},
	isSymLink: function(f) {
		return FS.statSync(f).isSymbolicLink();
	},
	stat: function(f) {
		// .size
		// .mtime, .mtimeMs
		// .atime, .atimeMs
		// .ctime, .ctimeMs
		return FS.statSync(f);
	},
	// フォルダを作成する
	create_folder: function(folder) {
		if (!FS.existsSync(folder)) {
			var oldMask = process.umask(0);
			//pverbose('process.umask: ' + process.umask().toString(8));
			FS.mkdirSync(folder, '0666');
			process.umask(oldMask);
			//pverbose('process.umask: ' + process.umask().toString(8));
			pverbose(folder+" was created.");
		}
	},
};

////////////////////////////////////////
// FileIO
// FileIO(file)
//   file - パスを指定する
function FileIO(file) {
	this._path = null;
	this._dir  = null;

	// パスを指定する
	this.set = function(file) {
		ptrace("FileIO.set:"+file);
		this._path = file;
		this._dir = PATH.dirname(file);
	};
	this.set(file);

	// ファイルがあるかどうか調べる
	this.exists = function() {
		ptrace("FileIO.exists:"+this._path);
		return FSUTL.exists(this._path);
	}

	// ファイルからデータをロードする
	this.load = function() {
		ptrace("FileIO.load:"+this._path);
		if (!this.exists(this._path)) {
			throw Exception("No such file.");
		}
		return JSON.parse(FS.readFileSync(this._path, 'utf8'));
	};

	// ファイルへデータを保存する
	this.save = function(d) {
		ptrace("FileIO.save:"+this._path);
		if (!FSUTL.exists(this._dir)) {
			this.create_folder(this._dir);
		}
		FS.writeFileSync(this._path, JSON.stringify(d));
	};

	// フォルダがない場合はフォルダを作成する
	this.create_folder = function(dir) {
		ptrace("FileIO.create_folder:"+dir);
		if (!FSUTL.exists(dir)) {
			FSUTL.create_folder(dir);
		}
	}
}

// DateObjを書式を指定して文字列に変換する
function date2string(dateObj, format) {
	//http://qiita.com/osakanafish/items/c64fe8a34e7221e811d0
  if (!format) format = 'YYYY-MM-DD hh:mm:ss.SSS';
  format = format.replace(/YYYY/g, dateObj.getFullYear());
  format = format.replace(/MM/g, ('0' + (dateObj.getMonth() + 1)).slice(-2));
  format = format.replace(/DD/g, ('0' + dateObj.getDate()).slice(-2));
  format = format.replace(/hh/g, ('0' + dateObj.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + dateObj.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + dateObj.getSeconds()).slice(-2));
  if (format.match(/S/g)) {
  	var milliSeconds = ('00' + dateObj.getMilliseconds()).slice(-3);
    var length = format.match(/S/g).length;
    for (var i = 0; i < length; i++) format = format.replace(/S/, milliSeconds.substring(i, i + 1));
  }
  return format;
}

////////////////////////////////////////
// Card
function Card(db)
{
	this._db = db;
	this._data = { _sys: {}, _user: {}, _code: {} };
	this._fileio = {data: null, history: null};
	this._dir = null;

	this.code = function(d) {
		var code = 0;
		switch(typeof(d)) {
			case "string":
				var hash = 0;
				for(var i=0; i < d.length; i++) {
					var charCode = d.charCodeAt(i);
					hash += ((charCode * 3) << 1);
				}
				code = hash;
				break;
			case "object":
				if (d == null) {
					code = 0;
				}
				break;
			case "number":
				code = d;
				break;
		}
		/*
		if (typeof(d) == "string") {
			var hash = 0;
			for(var i=0; i < d.length; i++) {
				var charCode = d.charCodeAt(i);
				hash += ((charCode * 3) << 1);
			}
			code = hash;
		} else if (typ{
			code = d;
		}
		*/
		//console.log("typeof()="+typeof(d)+", code="+code);
		return code;
	};
	

	// データの確定
	this.update_code = function() {
		ptrace("CDB::update_code: "+this._data._code);
		this._data._code.deleted = this.code(this._data._sys.deleted);
		this._data._code.locked  = this.code(this._data._sys.locked);
		for(var key in this._data._user) {
			this._data._code[key] = this.code(this._data._user[key]);
			//pverbose("=code["+key+"]="+this._data._code[key]);
		}
		//for(var key in this._data._code) {
		//	pverbose("code["+key+"]="+this._data._code[key]);
		//}
	};

    this.clear = function() {
		// system
		this._data._sys.id = 0;
		this._data._sys.pid = 0;
		this._data._sys.uid = uuid();
		this._data._sys.revision = 0;
		this._data._sys.deleted = 0;
		this._data._sys.date_created = Date.now();
		this._data._sys.date_modified = Date.now();
		this._data._sys.locked = null;
		// user
		this._data._user.closed = null; // open(null), closed(!null)
		this._data._user.name = null;
		this._data._user.wiki = null;
		this._data._user.due_date = null;
		this._data._user.owner = null;
		this._data._user.state = null;
		this._data._user.flags = null;
		//this._data._user.priority = 0;
		this._data._user.importantly = 0;
		this._data._user.urgency = 0;
		// copy
		if (this._db._switch.history == true) {
			for(var key in this._data._user) {
				this._data._code[key] = 0;
			}
		}
		//
		this.update_code();
	}
	this.clear();
	
	// データの比較
	this.differences = function() {
		var list = [];
		if (this._data._code.deleted != this.code(this._data._sys.deleted)) {
			console.log("Card.difference: ("+this._data._code.deleted+")..("+this.code(this._data._sys.deleted)+")");
			list.push({group: "sys", name: "delete", value: this._data._sys.deleted});
		}
		if (this._data._code.locked != this.code(this._data._sys.locked)) {
			list.push({group: "sys", name: "locked", value: this._data._sys.locked});
		}
		for(var key in this._data._user) {
			if (this._data._code[key] != this.code(this._data._user[key])) {
				list.push({group: "user", name: key, value: this._data._user[key]});
			}
		}
		return list;
	};

	// 文字化
	this.toString = function() {
		return "CDB["+this._db._box+"].Card["+this._sys.id+"]";
	}

	// ID構築
	this.build = function() {
		ptrace("Card::id");
		if (arguments.length > 0) {
			var id = arguments[0];
			var dir = PATH.dirname(this._db.get_storage_path());
			this._dir = dir+"/data/"+id;
			pverbose("dir="+dir);
			this._fileio.data = new FileIO(dir+"/data/"+id+"/data.json");
			this._fileio.history = new FileIO(dir+"/data/"+id+"/history.json");
			this._data._sys.id = id;
			if (typeof(this._data._sys.id) == "undefined") {
				throw new Error(this.toString()+": id is not defined.");
			}
			pverbose("id = "+this._data._sys.id);
		}
		return this._data._sys.id;
	};

	// 削除
	this.delete = function(who) {
		this.data._sys.deleted = who;
		this.update();
	};
	
	this.undelete = function(who) {
		if (who != this._sys.deleted) {
			throw new Error(this.toString()+": this is not deleted by you.");
		}
		this.data._sys.deleted = null;
		this.update();
	};
	
	this.isdeleted = function() {
		if (this.data._sys.deleted == null) {
			return false;
		} else {
			return true;
		}
	};

	// ロック
	this.lock = function(who) {
		this.data._sys.locked = who;
		this.update();
	};
	
    this.unlock = function(who) {
		if (who != this._sys.locked) {
			throw new Error(this.toString()+": this is not locked by you.");
		}
		this.data._sys.locked = null;
		this.update();
	};
	
	this.islocked = function() {
		if (this.data._sys.locked == null) {
			return false;
		} else {
			return true;
		}
	};
	
	// DB更新
	this.update = function() {
		ptrace("Card::update");
		var d = {_sys: {}, _user: {}};
		d._sys.id             = this._data._sys.id;
		d._sys.pid            = this._data._sys.pid;
		d._sys.uid            = this._data._sys.uid;
		d._sys.deleted        = this._data._sys.deleted;
		d._sys.date_modified  = this._data._sys.date_modified;
		d._sys.locked         = this._data._sys.locked;
		d._user.name          = this._data._user.name;
		d._user.due_date      = this._data._user.due_date;
		d._user.owner         = this._data._user.owner;
		d._user.state         = this._data._user.state;
		d._user.flags         = this._data._user.flags;
		d._user.importantly   = this._data._user.importantly;
		d._user.urgency       = this._data._user.urgency;

        // InMemoryDB更新
		var id = this._db.put(d); // CDB登録
		pverbose("Card::update: cardid="+id);
		this.build(id); // ID発番
		pverbose("Card::update: flush");
		this._db.flush(); //InMemoryDBフラッシュ
		
		// 変更履歴を保存する
		pverbose("Card::update: saving history");
		if (this._db._switch.history) {
			var who = "sys";
			if (arguments.length > 0) {
				who = arguments[0];
			}
			var diffs = this.differences();
			if (diffs.length != 0) {
				this.changelog(who, diffs);
				this.update_code(); // コードを更新する
			}
		}

		// カード情報保存
		this._fileio.data.save(this._data);
	};

	// カードの履歴情報を保存する
	this.changelog = function(who, diffs) {
		ptrace("Card.changelog");
		if (this._db._switch.history == false) {
			// 履歴不要
			return;
		}
		
		this._data._sys.revision++;
		
		var d =  {
			revision: this._data._sys.revision,
			date: Date.now(),
			who: who,
			changeset: []
		};
		if (this._data._sys.revision > 1) {
			d.changeset = diffs;
		}
		
		var h = [];
		if (this._fileio.history.exists() == true) {
			h = this._fileio.history.load();
		}
		h.push(d);
		// 履歴情報更新
		this._fileio.history.save(h);
	};

	// カード情報をロードする
	this.load = function() {
		ptrace("CDB::load");
		if (arguments.length > 0) {
			throw new Exception("Card.load: doesn't require arguments.");
		}
		if (this._data._sys.id == 0) {
			throw new Exception("Card.load: no id.");
		}
		this._data = this._fileio.data.load();
		
		if (this._db._switch.history == true) {
			this.update_code();
		}
	};
	
	// カード情報を保存する
	this.save = function() {
		var who = "sys";
		if (arguments.length > 0) {
			who = arguments[0];
		}
		
		this.update(who);
	};
	
	// カードシステム情報を取得する
	this.sys = function(name) {
		return this._data._sys[name];
	};
	
	// カードのユーザー情報を取得／設定する
	this.user = function() {
		if (arguments.length == 1) {
			return this._data._user[arguments[0]];
		} else if (arguments.length == 2) {
			this._data._user[arguments[0]] = arguments[1];
			return this._data._user[arguments[0]];
		} else {
			throw new Error(this.toString()+": couldn't set system field.");
		}
	};

    // HTML情報を読む
    this.html = function(accessor) {
        if (this._data._user.wiki == null) {
			return "";
		} else {
			return this.wiki2html(accessor);
		}
    }

    // カードにあるwiki(this._data._user.wiki)をhtml(this.html)に変換する
	this.wiki2html = function(accessor) {
		f = new WikiFormatter();
		f.set("parent", this._data._sys.id);
		f.set("name", this._data._user.name);
		f.set("user", accessor.user);
		f.set("handle", this._db);
		f.callback = WikiMacroExpander;
		
		REF.forget({type: "wiki" ,id: this._data._sys.id}); // FIXME: wiki?
		
		var html = f.convert(this._data._user.wiki);
		// $$はローカルフォルダに置換する
		//> "$$/image.png".replace(/(\$\$)/g, function($1,$2) { return "/1/2"; });
		// '/1/2/image.png'
		html = html.replace(/(\$\$)/g,
			(function(box,idd) {
				return function($1, $2) { return "public/data/"+box+"/data/"+id+"/"; };
			})(this._db.box, this._data._sys.id));
		return html;
	}
	
	// 添付ファイル情報
	this.attachment = function() {
		if (this._dir == null) {
			return "";
		}
		var result = [];
		var items = FS.readdirSync(this._dir);
		for(var i=0; i<items.length; i++) {
			pverbose("item="+items[i]);
			switch(items[i]) {
			case "data.json":
			case "history.json":
				continue;
				break;
			}
			
			var file = this._db._dir+"/data/"+this._data._sys.id+"/"+items[i];
			var stat = FSUTL.stat(file);
			result.push({
				url: this._db._urlbase+"/data/"+this._data._sys.id+"/"+items[i],
				name: items[i],
				mtime: stat.mtimeMs,
				size: stat.size,
			});
		}
		return result;
	};

	// 添付ファイルを削除する
	this.remove_attachment = function(name) {
	};

    // 履歴情報を読む
    this.history = function() {
    	if (this._db._switch.history == false) {
    		return;
    	}
    	
		console.log("history");
        var h = this._fileio.history.load();
		var result = [];
		for(var i=h.length-1; i >= 0; i--) {
			result.push(h[i]);
		}
		return result;
	};
}

////////////////////////////////////////
// Card DB
function CDB(p)
{
	this._version = "0.4";
	this._dir = null;
	this._path = null;
	this._cache = {};
	this._control = {};
	this._control.config = {};
	this._control.config.id = 1; // include entry 0
	this._control.config.date_created = Date.now();
	this._control.items = [];
	this._control.items.push({}); // entry 0
	this._switch = {};
	this._switch.history = false;

	if (typeof(p) == "undefined") {
		throw new Error("Cabinet.CDB has no parameter.");
	}

	ptrace("CDB");
	for(var i in p) {
		pverbose("CDB:spec."+i+"="+p[i]);
	}

	if (typeof(p.box) == "undefined") {
		throw new Error("Cabinet.CDB has no box parameter.");
	}

	if (typeof(p.dir) == "undefined") {
		throw new Error("Cabinet.CDB has no dir parameter.");
	}
	
	if (typeof(p.urlbase) == "undefined") {
		throw new Error("Cabinet.CDB has no url base dir parameter.");
	}

	this._box = p.box;
	this._urlbase = p.urlbase + "/" +p.box;
	this._dir = p.dir + "/" + p.box;
	this._path = this._dir + "/index.json";
	
	pverbose("CDB["+this._box+"].urlbase="+this._urlbase);
	pverbose("CDB["+this._box+"].dir="+this._dir);
	pverbose("CDB["+this._box+"].path="+this._path);

	if (typeof(p.history) != "undefined") {
		this._switch.history = p.history;
	}

	this.toString = function() {
		return "Cabinet.CDB["+this._box+"]";
	}

	pverbose("DB: "+ this.toString() + ":loading [history="+this._switch.history+"]");

	// ファイルパスを取得
	this.dir = function() {
		return this._dir;
	}
	
	// URL baseを取得
	this.urlbase = function() {
		return this._urlbase;
	}

	// 管理ファイル読込
	this.load = function() {
		ptrace("DB["+this._box+"].load:"+this._path);
		if (! FS.existsSync(this._path)) {
			pverbose("dosn't exist.");
			return 0;
		}
		this._control = JSON.parse(FS.readFileSync(this._path, "utf8"));
		pverbose("->loaded.");
		return 1;
	};

	// 管理ファイル保存
	this.save = function() {
		ptrace("DB["+this._box+"].save:"+this._path);
		this._control.config.date_modified = Date.now();
		FS.writeFileSync(this._path, JSON.stringify(this._control), "utf8");
		pverbose("->saved.");
		return 1;
	};
	
	// ファイルフラッシュ
	this.flush = function() {
		ptrace("DB["+this._box+"].flush");
		this.save();
	};

	// 環境値を設定/取得する
	this.config = function() {
		if (arguments.length == 0) {
			return null;
		}

		var name = arguments[0];

		if (arguments.length == 2) {
			this._control.config[name] = arguments[1];
		}

		if (typeof(this._control.config[name]) == "undefined") {
			return null;
		} else {
			return this._control.config[name];
		}
	};

	// DBの管理パスを取得する
	// idが指定された場合、idのパスを返す
	this.get_storage_path = function() {
		ptrace("DB["+this._box+"].get_storage_path");

		var id = 0;
		if (arguments.length > 0) {
			id = arguments[0];
		}

		// フォルダを取得
		var dir = PATH.dirname(this._path);

		// DBフォルダ作成
		if (!FS.existsSync(dir)) {
			FSUTL.create_folder(dir);
			pverbose(dir + " directory created.");
		}

		// dataフォルダ作成
		dir += "/data";
		if (!FS.existsSync(dir)) {
			FSUTL.create_folder(dir);
			pverbose(dir + " directory created.");
		}

		if (id != 0) {
			// IDフォルダ作成
			dir += "/" + id;
			if (!FS.existsSync(dir)) {
				FSUTL.create_folder(dir);
			}
		}

		// IDフォルダを返却
		return dir;
	};

	// 新しいIDを発番する
	this.newid = function() {
		ptrace("DB["+this._box+"].newid");
		var id = this._control.config.id;
		this._control.config.id++;
		this.save();
		return id;
	}

	// カードデータを作る/返す
	// idが指定された場合は、対応するカードデータを返す
	// 引数がない場合は、新規のカードを返す
	this.card = function() {
		ptrace(this.toString()+".card");
		var card = new Card(this);
		if (arguments.length != 0) {
			var id = arguments[0];
			card.build(id);
			card.load();
		}
		return card;
	};

	// idが存在するかどうか確認する
	this.exists = function(id) {
		ptrace(this.toString()+".exists");
		for(var i=0; i<this._control.items.length; i++) {
			if (typeof(this._control.items[i]._sys) != "undefined") {
				if (this._control.items[i]._sys.id == id) {
					return true;
				}
			}
		}
		return false;
	}

	// カードデータの一部(d)をInMemoryDBに登録/更新する
	// idが0(ない)場合には、発番して登録する
	this.put = function(d) {
		ptrace(this.toString()+".put");
		if (typeof(d) == "undefined") {
			throw new Error(this.toString()+".put: unhandle data.");
		}

		if (d._sys.id == 0) {
			pverbose("->creating");
			d._sys.id = this.newid();
			this._control.items.push(d);
		} else {
			pverbose("->writing");
			this._control.items[d._sys.id] = d;
		}
		return d._sys.id;
	};

	// InMemoryDB中のidに相当するカードデータの一部の参照を返す
	// データを変更するとInMemoryDBの内部のデータが変更されます。
	this.getAt = function(id) {
		ptrace(this.toString()+".getAt("+id+")");
		return this._control.items[id];
	};

	// InMemoryDB中のデータ項目数を返す
	this.length = function() {
		ptrace(this.toString()+".length");
		return this._control.items.length;
	};
	
	// InMemoryDB中のid番目のカードを削除する
	// フォルダ情報は削除しないので自分で削除する必要あり
	// この関数はREF用で、それ以外の使用では完全な削除の状態を作ることに注意すること
	this.remove = function(id) {
		this._control.items.splice(id, 1);
	};

	// InMemoryDB中のデータを走査し、1件単位に指定された関数を実行する
	// 関数が0を返却した場合は処理を停止する
	this.eval = function(f, arg) {
		ptrace(this.toString()+".eval");
		for(var i=0; i<this._control.items.length; i++) {
			var d = this._control.items[i];
			if (f(d, arg) == 0) {
				break;
			}
		}
	};

	// InMemoryDB中のデータを指定された検索関数で評価し、カードデータを集めて返却する
	// 検索関数が0を返す場合、検索に合致したと判断する
	this.find = function(f, arg) {
		ptrace(this.toString()+".find: "+this._control.items.length);
		var found = [];
		for(var i=1; i<this._control.items.length; i++) {
			var d = this._control.items[i];
			pverbose("check "+i+":"+(typeof(d) != "undefined")+","+(d._sys.deleted == 0));
			if (typeof(d) != "undefined" && d._sys.deleted == 0) {
				if (f(d, arg) == 0) {
					found.push(d);
				}
			}
		}
		return found;
	};

	// main logic
	if (this.load() == 0) {
		// new file
		this.get_storage_path();
		this.save();
	}
}

////////////////////////////////////////
// Stack
function Stack()
{
	this._stack = [];
	this.length = 0;
	this.push = function(d) {
		this._stack.push(d);
		this.length = this._stack.length;
	};
	this.getAt = function(i) {
		if (i >= this._stack.length) {
			console.trace();
			throw new TokenError();
		}
		return this._stack[i];
	};
	this.pop = function() {
		if (this._stack.length == 0) {
			return null;
		}
		var n = 1;
		if (arguments.length > 0) {
			n = 0;
		}
		var d = 0;
		for(var i=0; i<n; i++) {
			d = this._stack.pop();
		}
		this.length = this._stack.length;
		return d;
	};
	this.peek = function() {
		if (this._stack.length == 0) {
			return null;
		}
		var i = this.length-1;
		if (arguments.length > 0) {
			i -= arguments[0];
		}
		return this._stack[i];
	};
	this.clear = function() {
		this._stack.splice(0, this._stack.length);
		this.length = this._stack.length;
	};
	this.cutoff = function(from, to) {
		var partial = new Stack();
		for(var i=from; i<=to; i++) {
			partial.push(this._stack[i]);
		}
		this._stack.splice(from, to - from + 1);
		this.length = this._stack.length;
		return partial;
	};
	this.dump = function() {
		for(var i=0; i<this._stack.length; i++) {
			token.dump(this._stack[i]);
		}
	}
}

////////////////////////////////////////
// TextStreamReader
function TextStreamReader(text)
{
	this._text = text;
	this._cursor = 0;
	this._length = text.length;
	this.getc = function() {
		if (this._cursor >= this._length) {
			throw new TokenError(); // eof
		}
		var c = this._text.charAt(this._cursor);
		if (c == ' ') {
			if (this.skip_space() == 0) {
				throw new TokenError(); // eof
			}
			return c; // space
		}
		this._cursor++;
		return c; // char
	};
	this.ungetc = function() {
		this._cursor--;
	};
	this.skip_space = function() {
		while(this._cursor < this._length) {
			if (this._text.charAt(this._cursor) != ' ') {
				return 1;
			}
			this._cursor++;
		}
		return 0;
	};
}

////////////////////////////////////////
// Formatter
var token = {
	EOF: 			0,
	NOP: 			1,
	BOL: 			2,
	NEWLINE: 		3,
	SPACE: 			4,
	STRING:			5,
	SHARP:			6,
	EXCLAMATION: 	7,
	LSQUARE: 		8,
	RSQUARE: 		9,
	LPARENSIS: 		10,
	RPARENSIS: 		11,
	LBRACE: 		12,
	RBRACE: 		13,
	TAGB: 			14,
	TAGE: 			15,
	BAR: 			16,
	ASTERISK:		17,
	
	$ANY:			100,
	$STRINGS:		101,
	$FLUSH:			102,
	
	$PAGENAME_IN:	200,
	$PAGENAME_OUT:	201,
	$MACRO_IN:		202,
	$MACRO_OUT:		203,
	$HTAG:			204,
	$LITAG:			205,
	
	dump: function(d) {
		var str = "";
		switch(d.token) {
		case token.EOF: str = "[EOF]"; break;
		case token.NOP: str = "[NOP]"; break;
		case token.BOL: str = "[BOL]"; break;
		case token.NEWLINE: str = "[NEWLINE]"; break;
		case token.SHARP: str = "[SHARP]"; break;
		case token.STRING: str = "[STRING] = "+d.text; break;
		case token.SPACE: str = "[SPACE]"; break;
		case token.EXCLAMATION: str = "[EXCLAMATION]"; break;
		case token.LSQUARE: str = "[LSQUARE]"; break;
		case token.RSQUARE: str = "[RSQUARE]"; break;
		case token.LPARENSIS: str = "[LPARENSIS]"; break;
		case token.RPARENSIS: str = "[RPARENSIS]"; break;
		case token.LBRACE: str = "[LBRACE]"; break;
		case token.RBRACE: str = "[RBRACE]"; break;
		case token.BAR: str = "[BAR]"; break;
		case token.ASTERISK: str = "[ASTERISK]"; break;
		case token.$STRINGS: str = "<STRINGS> = "+d.text; break;
		case token.$ANY: str = "<ANY>"; break;
		case token.$FLUSH: str = "<FLUSH>"; break;
		case token.$PAGENAME_IN: str = "<PAGENAME_IN>"; break;
		case token.$PAGENAME_OUT: str = "<PAGENAME_OUT>"; break;
		case token.$MACRO_IN: str = "<MACRO_IN>"; break;
		case token.$MACRO_OUT: str = "<MACRO_OUT>"; break;
		case token.$HTAG: str = "<HTAG>"; break;
		case token.$HTAG: str = "<LITAG>"; break;
		}
		pverbose("token: "+str);
	},
};

////////////////////////////////////////
// Tokenizer
function Tokenizer(text)
{
	this.parse = function(text) {
		var tsr = new TextStreamReader(text);
		var stack = new Stack();
		
		stack.push({token: token.BOL, text: ""}); // virtual token
		
		var str = "";
		try {
			while(1) {
				var c = "";
				var cin = tsr.getc();
				switch(cin) {
				//case "#": 
					//c = tsr.getc();
					//if (c == " ") {
					//	tsr.ungetc();
					//	stack.push({token: token.SHARP, text: "#"});
					//} else {
					//	str += "#"+c;
					//}
					//break;
				//case "!": stack.push({token: token.EXCLAMATION, text: "!"}); break;
				//case "[": stack.push({token: token.LSQUARE, text: "["}); break;
				//case "]": stack.push({token: token.RSQUARE, text: "]"}); break;
				//case "(": stack.push({token: token.LPARENSIS, text: "("}); break;
				//case ")": stack.push({token: token.RPARENSIS, text: ")"}); break;
				//case "{": stack.push({token: token.LBRACE, text: "{"}); break;
				//case "}": stack.push({token: token.RBRACE, text: "}"}); break;
				case "|": 
					stack.push({token: token.BAR, text: "|"});
					c = tsr.getc();
					if (c == "_") {
						var d = stack.peek();
						d.text = "|_";
					} else if (c == ".") {
						var d = stack.peek();
						d.text = "|.";
					} else {
						tsr.ungetc();
					}
					break;
				//case "*": 
					//console.log("*");
					//c = tsr.getc();
					//if (c == " ") {
					//	if (str.length > 0) {
					//		str += "*";
					//		tsr.ungetc();
					//	} else {
					//		stack.push({token: token.ASTERISK, text: "*"});
					//	}
					//} else {
					//	str += "*";
					//	tsr.ungetc();
					//}
					//break;
				case " ": 
					if (str.length > 0) {
						//if (str == "**") {
						//	stack.push({token: token.ASTERISK, text: "*"});
						//	stack.push({token: token.ASTERISK, text: "*"});
						//} else {
							str = this.decoration(str);
							stack.push({token: token.STRING, text: str});
						//}
					}
					str = "";
					break;
				case "\r": break; // ignore, continue
				case "\n":
					if (str.length > 0) {
						str = this.decoration(str);
						stack.push({token: token.STRING, text: str});
					}
					str = "";
					stack.push({token: token.NEWLINE, text: ""});
					stack.push({token: token.BOL, text: ""}); // virtual token
					break;
				case "%":
					c = tsr.getc();
					if (c == "{") {
						// page
						var pagename = "";
						while(1) {
							c = tsr.getc();
							if (c == "}") {
								c = tsr.getc();
								if (c == "%") {
									break;
								}
								pagename += "}" + c;
							} else {
								pagename += c;
							}
						}
						if (str.length > 0) {
							//str = this.decoration(str);
							stack.push({token: token.STRING, text: str});
							str = "";
						}
						stack.push({token: token.$PAGENAME_IN, text: ""});
						stack.push({token: token.STRING, text: pagename});
						stack.push({token: token.$PAGENAME_OUT, text: ""});
					} else if (c == "[") {
						// macro
						var macro = "";
						while(1) {
							c = tsr.getc();
							if (c == "]") {
								c = tsr.getc();
								if (c == "%") {
									break;
								}
								macro += "]" + c;
							} else {
								macro += c;
							}
						}
						if (str.length > 0) {
							//str = this.decoration(str);
							stack.push({token: token.STRING, text: str});
							str = "";
						}
						stack.push({token: token.$MACRO_IN, text: ""});
						stack.push({token: token.STRING, text: macro});
						stack.push({token: token.$MACRO_OUT, text: ""});
					} else {
						str += "%" + c;
					}
					break;
				default:
					str += cin;
					break;
				}
			}
		} catch(e) {
			if (c != "") {
				str += c;
			}
			if (str != "") {
				str = this.decoration(str);
				stack.push({token: token.STRING, text: str});
			}
			stack.push({token: token.NEWLINE, text: ""}); // virtual
			stack.push({token: token.EOF, text: ""});
		} finally {
			return stack;
		}
	};
	this.decoration = function(str) {
		str = this.replace(str, "~~", "<span style='text-decoration: line-through;'>", "</span>");
		str = this.replace(str, "__", "<span style='text-decoration: underline;'>", "</span>");
		str = this.replace(str, "**", "<span style='font-weight: bold;'>", "</span>");
		str = this.replace(str, "_", "<span style='font-style: italic;'>", "</span>");
		str = this.replace(str, "*", "<span style='color: #ff0000;'>", "</span>");
		
		// /^(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$/
		var body = str.replace(/(http|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)/g, function($1, $2) {
			return "<a href='"+$1+"'>"+$1+"</a>";
		});
		str = body;
		
		return str;
	}
	this.replace = function(text, key, firstWord, lastWord) {
		var p, q;
		p = text.indexOf(key, 0);
		q = text.indexOf(key, p+key.length+1);
		if (arguments.length == 5) {
			if (arguments[4] == 1) {
				trace("text="+text+", key="+key+", p="+p+", q="+q);
			}
		}
		if ((p >= 0) && (q >= p)) {
			text = text.replace(key, firstWord).replace(key, lastWord);
		}
		return text;
	};
}

////////////////////////////////////////
// wiki macro expander
// %{...}%
// %[...]%
// WikiMacroExpander(formatter, type=%{}%, %[]%, value=...)
function WikiMacroExpander(f, type, value)
{
	ptrace("wiki.default_expander");
	pverbose("type="+type+", value="+value);
	var body = "";
	switch(type) {
	case "%{":
		var found;
		var h = f.formatter.get("handle");
		var currentId = f.formatter.get("parent");
		var currentName = f.formatter.get("name");

		var expanding = false;
		var isWiki; // カード参照(@[:]<num>)=trueか、タイトル=falseか
		if (value.charAt(0) == '@') {
			pverbose("macro: card:");
			// カード参照
			isWiki = false
			// valueの先頭に":"がある場合は、情報の展開指示になる
			if (value.charAt(1) == ':') {
				cid = value.substring(2);
				expanding = true;
			} else {
				cid = value.substring(1);
			}
			found = [];
			found.push(h.card(cid));
		} else {
			pverbose("macro: wiki: "+value);
			// タイトル
			isWiki = true;
			found = h.find(function(d, arg) {
				if (d._user.name == arg) {
					return 0;
				} else {
					return 1;
				}
			}, value);
		}

		pverbose("macro: found? -> "+found.length);
		if (found.length == 0) {
			if (isWiki == true) {
				body = "<span class='ko2wiki unknown' data-id=0 data-pid=0>"+value+"</span>";
			} else {
				body = "<font color=red>カード番号"+cid+"がありません。</font>";
			}
		} else {
			if (isWiki == true) {
				body = "<span class='ko2wiki' data-box='wiki'"
					+ " data-id="+found[0]._sys.id
					+ " data-pid="+found[0]._sys.pid
					+">"+value+"</span>";
					REF.add({type: "wiki", id: currentId, name: currentName}, {type: "wiki", id: found[0]._sys.id, name: value});
			} else {
				body = "<span class='ko2wiki' data-box='task'"
					+ " data-id="+found[0]._sys.id +
					+ " data-pid="+found[0]._sys.pid +
					+ " data-flags='"+found[0]._user.flags+"'"
					+ " data-owner='"+found[0]._user.owner+"'"
					+ " data-state='"+found[0]._user.state+"'"
					+ " data-substate='"+found[0]._user.substate+"' ";
					+ ">"+ value + "</span>";
					REF.add({type: "task", id: currentId, name: currentName}, {type: "task", id: found[0]._sys.id, name: value});
			}
			REF.flush();
		}
		return body;
	case "%[":
		/*
		var str = value.replace(/%user%/g, f.formatter.get("user"));
		var str = value.replace(/%parent%/g, f.formatter.get("parent"));
		var box = f.formatter.get("box");
		pverbose("matcher="+str);
		var matcher = eval('('+str+')');
		found = cabinet.box(box).find(matcher, null);
		var body = "";
		for(var i=0; i<found.length; i++) {
			var style = "doing";
			if (found[i].conclusion != "") {
				style = "done";
			}
			body += "<a href='/page?id="+box+"&cid="+found[i]._sys.id+"&cmd=view' class="+style+">"+
				"(@"+found[i]._sys.id+") "+
				found[i].title+
				"</a><br>\n";
		}
		utils.pverbose(body);
		return body;
		*/
		break;
	}
	return "[parse error.]";
}

////////////////////////////////////////
// Formatter
function WikiFormatter()
{
	this.prop = {};
	this.set = function(name, value) {
		this.prop[name] = value;
	};
	this.get = function(name) {
		return this.prop[name];
	};

	this.callback = function() { return ""; };

	this.syntax = [
		{ id: "SPC01",
			sequence: [ token.$PAGENAME_IN, token.STRING, token.$PAGENAME_OUT ],
			op: function(p) {
				var str = p.callback(p, "%{", p.substack.getAt(1).text);
				p.stack.push({token: token.STRING, text: str});
				return "";
			}
		},
		{ id: "SPC02",
			sequence: [ token.$MACRO_IN, token.STRING, token.$MACRO_OUT ],
			op: function(p) {
				var str = p.callback(p, "%[", p.substack.getAt(1).text);
				p.stack.push({token: token.STRING, text: str});
				return "";
			}
		},
		{ id: "HTG01",
			sequence: [ token.BOL, token.$HTAG, token.$STRINGS, token.NEWLINE ], 
			op: function(p) {
				return "<"+p.substack.getAt(1).text+">"+p.substack.getAt(2).text+"</"+p.substack.getAt(1).text+">\n";
			}
		},
		{ id: "ULI01",
			sequence: [ token.BOL, token.$LITAG, token.$STRINGS, token.NEWLINE ], 
			op: function(p) {
				var n = Number(p.substack.getAt(1).text.substring(2));
				var body = "";
				for(var i=0; i<n; i++) {
					body += "<ul>";
				}
				body += "\n<li>"+p.substack.getAt(2).text+"</li>\n";
				for(var i=0; i<n; i++) {
					body += "</ul>";
			}
			body += "\n";
			return body;
		}
		},
		{ id: "TBL01",
			sequence: [ token.BOL, token.BAR ],
			op: function(p) {
				p.stack.push({token: token.BAR, text: p.substack.getAt(1).text});
				return "<tr>";
			}
		},
		{ id: "TBL02",
			sequence: [ token.BAR, token.$STRINGS, token.BAR ],
			op: function(p) {
				p.stack.push({token: token.BAR, text: p.substack.getAt(2).text});
				var tagTD = "";
				if (p.substack.getAt(0).text == "|_") {
					tagTD = "<td style='background: #acacff;'>"
				} else if (p.substack.getAt(0).text == "|.") {
					tagTD = "<td style='background: #c0c0c0;'>"
				} else {
					tagTD = "<td>";
				}
				return tagTD+p.substack.getAt(1).text+"</td>";
			}
		},
		{ id: "TBL03",
			sequence: [ token.BAR, token.NEWLINE ],
			op: function(p) {
				return "</tr>\n";
			}
		},
		{ id: "STR01",
			sequence: [ token.BOL, token.STRING ],
			op: function(p) {
				var hcount = 0;
				var acount = 0;
				var len = p.substack.getAt(1).text.length;
				for(var i=0; i<len; i++) {
					if (p.substack.getAt(1).text.charAt(i) == '#') {
						hcount++;
					} else if (p.substack.getAt(1).text.charAt(i) == '*') {
						acount++;
					}
				}
				pverbose("hcount="+hcount+", acount="+acount+", len="+len);
				if (hcount == len) { // '#
					if (hcount > 0) {
						p.stack.push({token: token.BOL, text: ""});
						p.stack.push({token: token.$HTAG, text: "H"+hcount});
					}
				} else if (acount == len) {
					if (acount > 0) {
						p.stack.push({token: token.BOL, text: ""});
						p.stack.push({token: token.$LITAG, text: "LI"+acount});
					}
				} else {
					if (len > 0) {
						p.stack.push({token: token.STRING, text: p.substack.getAt(1).text});
				}
			}
			return "";
		}
		},
		{ id: "STR02",
			sequence: [ token.$STRINGS, token.NEWLINE ],
			op: function(p) {
				return p.substack.getAt(0).text+"<br>\n";
			}
		},
		{ id: "STR03",
			sequence: [ token.$STRINGS, token.$ANY ],
			op: function(p) {
				p.stack.push({token: token.$STRINGS, text: p.substack.getAt(0).text+" "+p.substack.getAt(1).text});
				return "";
			}
		},
		{ id: "STR04",
			sequence: [ token.STRING, token.STRING ],
			op: function(p) {
				p.stack.push({token: token.$STRINGS, text: p.substack.getAt(0).text+" "+p.substack.getAt(1).text});
				return "";
			}
		},
		{ id: "STR05",
			sequence: [ token.BOL, token.NEWLINE ],
			op: function(p) {
				return "<br>\n";
			}
		},
		{ id: "STR06",
			sequence: [ token.$STRINGS, token.NEWLINE ],
			op: function(p) {
				return p.substack.getAt(0).text+"<br>\n";
			}
		},
		{ id: "STR07",
			sequence: [ token.NEWLINE ],
			op: function(p) {
				return "<br>\n";
			}
		},
		{ id: "STR08",
			sequence: [ token.$STRINGS, token.$FLUSH ],
			op: function(p) {
				return p.substack.getAt(0).text + p.substack.getAt(1).text;
			}
		},
		{ id: "STR09",
			sequence: [ token.$STRINGS, token.$EOF ],
			op: function(p) {
				return p.substack.getAt(0).text;
			}
		},
	];
	
	this.dump = function(title, stack) {
		var result = title + ":";
		for(var i=0; i<stack.length; i++) {
			switch(stack.getAt(i).token) {
			case token.EOF:			result += "[EOF] "; break;
			case token.NOP:			result += "[NOP] "; break;
			case token.BOL:			result += "[BOL] "; break;
			case token.NEWLINE:		result += "[CR ] "; break;
			case token.SPACE:		result += "[SPC] "; break;
			//case token.STRING:		result += "[STR] "; break;
			case token.STRING:		result += "[STR("+stack.getAt(i).text+")] "; break;
			case token.SHARP:		result += "[#  ] "; break;
			case token.EXCLAMATION:	result += "[!  ] "; break;
			case token.LSQUARE:		result += "[[  ] "; break;
			case token.RSQUARE:		result += "[]  ] "; break;
			case token.LPARENSIS:	result += "[(  ] "; break;
			case token.RPARENSIS:	result += "[)  ] "; break;
			case token.LBRACE:		result += "[{  ] "; break;
			case token.RBRACE:		result += "[}  ] "; break;
			case token.TAGB:		result += "[TGB] "; break;
			case token.TAGE:		result += "[TGE] "; break;
			case token.BAR:			result += "[|  ] "; break;
			case token.ASTERISK:	result += "[*  ] "; break;
			case token.$ANY:		result += "<ANY> "; break;
			//case token.$STRINGS:	result += "<STR> "; break;
			case token.$STRINGS:	result += "<STR("+stack.getAt(i).text+")> "; break;
			case token.$FLUSH:		result += "<FLS> "; break;
			case token.$PAGENAME_IN: result += "<PGI> "; break;
			case token.$PAGENAME_OUT: result += "<PGO> "; break;
			case token.$MACRO_IN: 	result += "<MCI> "; break;
			case token.$MACRO_OUT: 	result += "<MCO> "; break;
			case token.$HTAG: 		result += "<HTAG("+stack.getAt(i).text+")> "; break;
			case token.$LITAG: 		result += "<LITAG("+stack.getAt(i).text+")> "; break;
			}
		}
		pverbose(result);
	};
	
	this.compare_token = function(t1, t2) {
		if ((t1 == token.$ANY) || (t2 == token.$ANY)) {
			if (
				(t1 != token.BOL) && (t2 != token.BOL) &&
				(t1 != token.BAR) && (t2 != token.BAR) &&
				(t1 != token.$FLUSH) && (t2 != token.$FLUSH) &&
				(t1 != token.$PAGENAME_IN) && (t2 != token.$PAGENAME_IN) &&
				(t1 != token.$MACRO_IN) && (t2 != token.$MACRO_IN)
			) {
				return true;
			}
		}
		if ((t1 == token.STRING) || (t1 == token.$STRINGS)) {
			if ((t2 == token.STRING) || (t2 == token.$STRINGS)) {
				return true;
			}
		}
		return t1 == t2;
	};
	this.convert = function(text) {
		var tokenizer = new Tokenizer();
		var stream0 = tokenizer.parse(text);
		//stream.dump();
		//pverbose(">>>>>");
		
		// reconstruct ul/li and table
		var tbl = 0;
		var skip = 0;
		var li = 0;
		var stream = new Stack();
		for(var i=0; i<stream0.length; i++) {
			skip = 0;
			
			// /ul/li
			if ((i < stream0.length-2) &&
				(stream0.getAt(i).token == token.BOL) && 
				(stream0.getAt(i+1).token == token.STRING) &&
				(stream0.getAt(i+1).text.charAt(0) == "*")) {
				var nasterisks = 0;
				for(var j=0; j<stream0.getAt(i+1).text.length; j++) {
					if (stream0.getAt(i+1).text.charAt(j) == "*") {
						nasterisks++;
					}
				}
				if (nasterisks == stream0.getAt(i+1).text.length) {
					if (li == 0) {
						
					} else if (li < nasterisk) {
					} else if (li > nasterisk) {
					} else {
						stream.push({token: token.STRING, text: "<li>"+stream0.getAt(i+2).text.length+"</li>"});
					}
				}
			} else if (li > 0) {
			}
			
			// table
			if ((i < stream0.length-2) &&
				(stream0.getAt(i).token == token.BOL) && 
				(stream0.getAt(i+1).token == token.BAR)) {
				if (tbl == 0) {
					tbl = 1;
					stream.push({token: token.STRING, text: "<table class=modern>"});
					stream.push({token: token.$FLUSH, text: "\n"});
					stream.push(stream0.getAt(i++));
					stream.push(stream0.getAt(i++));
				}
				while(1) {
					stream.push(stream0.getAt(i++));
					if ((stream0.getAt(i).token == token.NEWLINE) || (stream0.getAt(i).token == token.EOF)) {
						break;
					}
				}
			} else {
				if (tbl == 1) {
					tbl = 0;
					stream.push({token: token.STRING, text: "</table>"});
					stream.push({token: token.$FLUSH, text: "\n"});
				}
			}
			
			if (skip == 0) {
				stream.push(stream0.getAt(i));
			}
		}
		
		stream.dump();
		pverbose("-----");
		
		var result = "";
		var stack = new Stack();
		var found = null; 
		var stack_bp = 0;
		for(var i=0; i<stream.length; i++) {
			stack.push(stream.getAt(i));
			
			// verify the syntax
			this.dump(">stack", stack);
			while(1) {
				found = null;
				stack_bp = 0;
				for(var j=0; j<this.syntax.length; j++) {
					if (stack.length < this.syntax[j].sequence.length) {
						continue;
					}
					var matched = 2; // 0=no, 1=partial, 2=full
					stack_bp = stack.length - this.syntax[j].sequence.length;
					for(var k = 0; k<this.syntax[j].sequence.length; k++) {
						var d = stack.getAt(stack_bp+k);
						if (typeof d == "undefined") {
							continue;
						}
						if (!this.compare_token(d.token, this.syntax[j].sequence[k])) {
							matched = 0;
							break;
						}
					}
					if (k != this.syntax[j].sequence.length) {
						matched = 1;
					}
					if (matched == 2) {
						found = j;
						break;
					}
				}
				
				if (found != null) {
					var substack = stack.cutoff(stack_bp, stack.length-1);
					this.dump(">>substack("+this.syntax[found].id+")", substack);
					result += this.syntax[found].op({stack: stack, substack: substack, callback: this.callback, formatter: this});
				} else {
					break; // no match
				}
			}
		}
		for(var i=0; i<stack.length; i++) {
			result += stack.getAt(i).text;
		}
		
		var body = result;
		
		// /^(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$/
		//var body = result.replace(/(http|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)/g, function($1, $2) {
		//	return "<a href='"+$1+"'>"+$1+"</a>";
		//});
		
		return body;
		//return result;
	};
}

////////////////////////////////////////
// Diary
var DIARY = {
	_dbh: null,
	initialize: function() {
		DIARY._dbh = new CDB({box: "diary", dir: config.path.database, urlbase: config.path.urlbase, history: false});
	},
	set: function(y, m, d, data) {
		DIARY._dbh.put({year: y, month: m, day: d, data: data});
	},
	get: function(y, m, d) {
		return DIARY._dbh.find(
			function(d, arg) {
				if ((d._data._user.year  == arg.year) &&
					(d._data._user.month == arg.month) &&
					(d._data.day == arg.day)) {
					return 0;
				} else {
					return 1;
				}
			},
			{ year: y, month: m, day: d }
		);
	},
	list: function(y, m) {
		return DIARY._dbh.find(
			function(d, arg) {
				if ((d._data._user.year  == arg.year) &&
					(d._data._user.month == arg.month)) {
					return 0;
				} else {
					return 1;
				}
			},
			{ year: y, month: m }
		);
	}
};

////////////////////////////////////////
// Reference
var REF = {
	_dbh: null,
	initialize: function() {
		REF._dbh = new CDB({box: "ref", dir: config.path.database, urlbase: config.path.urlbase, history: false});
	},
	record: function() {
		// 各DBに対してオブジェクトの定義(box含む)と比較メソッド等
		var data = {};
		data._sys = { id: 0, deleted: 0 };
		data._user = { parent: null, children: [] };
	},
	forget: function(parent) {
		ptrace("REF::forget");
		// parentが持つ参照関係を消去する
		var found = REF._dbh.find(
			function(d, arg) {
				if ((d._user.parent.type  == arg.type) &&
					(d._user.parent.id    == arg.id)) {
					return 0; // match
				} else {
					return 1; // unmatch
				}
			}, 
			parent
		);
		
		for(var i=0; i<found.length; i++) {
			REF._dbh.remove(found[i]._user.parent.id);
		}
	},
	add: function(parent, child) {
		ptrace("REF::add: "+REF._dbh);
		// 親を探す
		var found = REF._dbh.find(
			function(d, arg) {
				if ((d._user.parent.type  == parent.type) &&
					(d._user.parent.id    == parent.id)) {
					return 0; // match
				} else {
					return 1; // unmatch
				}
			},
			parent
		);
		if (found.length > 0) {
			// 1つしかない筈
			found[0]._user.children.push(child);
		} else if (found.length == 0) {
			// 新規
			REF.set(parent, child);
		}
	},
	set: function(parent, children) {
		ptrace("REF::set");
		// parent(box, id)->children(box, id)の関係を構築する
		REF._dbh.put({_sys: {id: 0, deleted: 0}, _user: { parent: parent, children: [ children ] } });
		REF._dbh.flush();
	},
	find: function(child) {
		ptrace("REF.find child.type="+child.type+", id="+child.id);
		// cid(type, id)を参照しているコンテンツ(オブジェクトのリスト)を返す
		var rlist = REF._dbh.find(
			function(d, arg) {
				var found = 0;
				for(var i=0; i<d._user.children.length; i++) {
					var child = d._user.children[i];
					console.log("child.type="+child.type+", id="+child.id);
					if ((child.type  == arg.type) &&
						(child.id    == arg.id)) {
						return 0;
					}
				}
				return 1;
			},
			child
		);
		var result = [];
		for(var i=0; i<rlist.length; i++) {
			var r = rlist[i];
			result.push({
				type: r._user.parent.type, 
				id: r._user.parent.id, 
				pid: r._user.parent.pid,
				name: r._user.parent.name
			 });
		}
		return result;
	},
	flush: function() {
		REF._dbh.flush();
	}
};

////////////////////////////////////////
// Wiki
var WIKI = {
	_dbh: null,
	initialize: function() {
		WIKI._dbh = new CDB({box: "wiki", dir: config.path.database, urlbase: config.path.urlbase, history: true});
		
		var checker = function(d, arg) {
			if (d._user.name == arg) {
				return 0; // match
			} else {
				return 1; // unmatch
			}
		};
		
		var found = WIKI._dbh.find(checker, "front");
		if (found.length == 0) {
			var card = new Card(WIKI._dbh);
			card.user("name", "front");
			card.user("wiki", "# Front page");
			card.save();
		}
		
		var found = WIKI._dbh.find(checker, "admin");
		if (found.length == 0) {
			var card = new Card(WIKI._dbh);
			card.user("name", "admin");
			card.user("wiki", "# @admin");
			card.save();
		}
	},
	path(id) {
		return WIKI._dbh.dir() + "/data/" + id;
	},
	card: function(id) {
		return WIKI._dbh.card(id);
	},
	getCardData: function(card, user) {
		// 基本情報
		var result = {};
		result.id			= card.sys("id");
		result.pid			= card.sys("pid");
		result.name			= card.user("name");
		result.closed		= card.user("closed");
		// 本文、HTML化した本文
		result.wiki			= card.user("wiki");
		result.html 		= card.html(user); // 参照関係を再構築する
		// 添付
		result.attachment	= card.attachment();
		
		// 参照関係
		result.reference	= REF.find({type: "wiki", id: result.id});
		
		return result;
	},
	set: function(data, user) {
		ptrace("WIKI.set: id="+data.id+", user="+user);
		var card;
		if (data.id == 0) {
			pverbose("WIKI.set:new card");
			card = new Card(WIKI._dbh);
		} else {
			pverbose("WIKI.set:get card");
			card = WIKI._dbh.card(data.id);
		}
		pverbose("WIKI.set:saving");
		card._data._sys.pid = data.pid;
		card.user("name", data.name);
		card.user("wiki", data.wiki);
		card.save(user);
		pverbose("WIKI.set:done.");
		
		pverbose("WIKI.set:reload");
		// reload
		return WIKI.get(card._data._sys.id, user);
	},
	get: function(id, user) {
		ptrace("WIKI.get: id="+id+", user="+user);
		var card = WIKI._dbh.card(id);
	
		var result = this.getCardData(card, user);
		
		return {card: result};
	},
	history: function(id) {
		var card = WIKI._dbh.card(id);
		
		return { history: card.history() };
	},
	list: function() {
		var result = [];
		for(var i=0; i<WIKI._dbh.length(); i++) {
			var d = WIKI._dbh.getAt(i);
			result.push(
				{
					id:				d._sys.id,
					pid:			d._sys.pid,
					name:			d._user.name,
					closed:			d._user.closed,
				}
			);
		}
		return {list: result};
	},
	search: function(name) {
		ptrace("WIKI.search("+name+")");
		var found = WIKI._dbh.find(
			function(d, arg) {
				console.log("=> "+d.name+","+arg);
				if (d._user.name == arg) {
					return 0; // match
				} else {
					return 1; // unmatch
				}
			},
			name
		);
		pverbose("WIKI.search: result="+found.length);
		if (found.length > 0) {
			var result = WIKI.get(found[0]._sys.id, "sys");
			result.name = name;
			return result;
		} else {
			return {card: null, name: name};
		}
	}
};

////////////////////////////////////////
// Task
var TASK = {
	_dbh: null,
	initialize: function() {
		TASK._dbh = new CDB({box: "task", dir: config.path.database, urlbase: config.path.urlbase, history: true});
	},
	set: function(id, data) {
	},
	get: function(id) {
	},
	history: function(id) {
	},
	list: function() {
	},
};


////////////////////////////////////////
// Diary
var Diary = {
	get_days: function(context) {
	},
	set_diary: function(context) {
	},
	get_diary: function(context) {
	}
};

////////////////////////////////////////
// Wiki
var Wiki = {
	list: function(context) {
		context._res.send({list: JSON.stringify(WIKI.list())});
	},
	set: function(context) {
		ptrace("Wiki::set");
		var card = JSON.parse(context.param("card"));
		var user = context.cookie("user");
		console.log("user="+user+", card="+card);
		//context.dump();
		context._res.send(JSON.stringify(WIKI.set(card, user)));
	},
	get: function(context) {
		context._res.send(JSON.stringify(WIKI.get(context.param("id"), context.cookie("user"))));
	},
	history: function(context) {
		context._res.send(JSON.stringify(WIKI.history(context.param("id"))));
	},
	search: function(context) {
		context._res.send(JSON.stringify(WIKI.search(context.param("name"))));
	},
	upload: function(context) {
		// https://qiita.com/smatsui@github/items/bb44f7f3f33448479841
		ptrace("Wiki::upload");
		
		config.env.uploader(context._req, context._res, function(err) {
			var target_folder = WIKI.path(context.param("id")) + "/";
			var tmpfile = context._req.file.destination + context._req.file.filename;
			var orgfile = target_folder + context._req.file.originalname;
			var bytes = context._req.file.size;
		
			pverbose(" ->tmp="+tmpfile);
			pverbose(" ->org="+orgfile);
			
			FS.renameSync(tmpfile, orgfile);
			pverbose("File uploaded to: " + orgfile + " - " + bytes + ' bytes');
			
			var card = WIKI.card(context.param("id"));
			var result = {status: "OK"};
			result.attachment = card.attachment();
			context._res.send(JSON.stringify(result));
		});
	},
	download: function(context) {
		ptrace("session.download: ");
		pverbose(" ->downloading ... "+context._req.url);
		//http://qiita.com/_shimizu/items/094c4beace9c7a36deb1
		// http://stackoverflow.com/questions/37367370/nodejs-after-second-post-get-500
		//クロスドメイン対応
		var header = {
			"Access-Control-Allow-Origin":"*",
			"Pragma": "no-cache",
			"Cache-Control" : "no-cache"
		};
		var filename = decodeURI(context._req.url).substring(1);
		var contents = fs.readFileSync(filename, "binary");
		context._res.writeHead(200, header);
		context._res.write(contents, "binary");
		context._res.end();
	}
};

////////////////////////////////////////
// Task
var Task = {
	list: function(context) {
	},
	list_open: function(context) {
	},
	list_close: function(context) {
	},
	set: function(context) {
	},
	get: function(context) {
	},
	children: function(context) {
	},
};

////////////////////////////////////////
// Task
var User = {
	get: function(context) {
		username = context.param("user")
		ptrace("User::get("+username+")");
		var found = WIKI._dbh.find(
			function(d, arg) {
				console.log("=> "+d._user.name+","+arg);
				if (d._user.name == arg) {
					return 0; // match
				} else {
					return 1; // unmatch
				}
			},
			username
		);
		var result = {};
		pverbose("User::get: result="+found.length);
		if (found.length > 0) {
			pverbose("loading user page."+found[0]._sys.id);
			result = WIKI.get(found[0]._sys.id, "sys");
		} else {
			// 正規ユーザーで且つ最初のアクセス=ページ作成
			pverbose("creating use page.");
			var card = new Card(WIKI._dbh);
			card.user("name", username);
			card.user("wiki", "# "+username);
			card.save();
			
			result = {card: WIKI.getCardData(card, username)};
		}
		str = JSON.stringify(result);
		console.log("="+str);
		context._res.send(str);
	}
};

////////////////////////////////////////
// Home
function Home(context)
{
	context._res.render("home",
		{
			prog_name: config.prog.name,
			prog_version: config.prog.version,
			user: context._user.name
		}
	);
}

////////////////////////////////////////
// Login
var login_message = "";
function Login(req, res)
{
	res.render("login", 
		{
			site_name: site.config.name,
			site_description: site.config.description,
			site_version: site.config.version,
			site_copyright: site.config.copyright,
			message: login_message,
			prog_name: config.prog.name,
			prog_version: config.prog.version,
			prog_description: config.prog.description,
			prog_copyright: config.prog.copyright
		}
	);
}

////////////////////////////////////////
// Logout
function Logout(req, res)
{
	ptrace("Logout");
	res.clearCookie('user');
	if (site.config.session.expire != 0) {
		res.clearCookie('expire');
	}
	//Login(req, res);
	res.redirect("/login");
}

////////////////////////////////////////
// Context
function Context(req, res, next)
{
	ptrace("Context");

	this._req = req;
	this._res = res;
	this._param = {};
	
	// cookie
	var cookies = {};
	req.headers.cookie && req.headers.cookie.split(';').forEach(function( cookie ) {
		var parts = cookie.split('=');
		cookies[ parts[ 0 ].trim() ] = ( parts[ 1 ] || '' ).trim();
	});
	this._cookies = cookies;
	
	// param
	this._param = {};
	// method=get
	for(var k in req.query) {
		//console.log("query["+k+"]="+req.query[k]);
		this._param[k] = req.query[k];
	}
	// method=post
	for(var k in req.body) {
		//console.log("body["+k+"]="+req.body[k]+", "+typeof(req.body[k]));
		this._param[k] = req.body[k];
	}
	// url, route/:param を展開
	for(var k in req.params) {
		//console.log("params["+k+"]="+req.params[k]);
		this._param[k] = req.params[k];
	}
	
	// パラメータを取得
	this.param = function(name) {
		return this._param[name];
	};
	
	// パラメータのダンプ
	this.dump = function() {
		console.log("= context dump =");
		for(var k in this._param) {
			console.log(k+"="+this._param[k]);
		}
	};
	
	// パラメータの存在を確認
	this.exists = function(name) {
		return typeof(this._param[name]) != "undefined";
	};
	
	// クッキーを取得
	this.cookie = function(name) {
		return this._cookies[name];
	};
	
	this.validation = function() {
		var _func_ = "Context.validation";
		ptrace(_func_);
		
		var has_cookie = false;
		login_message = "";
		
		// user validation
		this._user = null;
		if (typeof(this._cookies.user) == "undefined") {
			pverbose(_func_+": no name in cookie.");
			// not login
			// but, it has login information
			if (typeof(this._param.user) == "undefined") {
				// no
				pwarning(_func_+": failure");
				return false;
			}
			pverbose(_func_+": username in parameter");
			// has username in parameters, need authentication
			login_message = "Invalid user name or password.<br>Please retry.<p>";
			for(var i=0; i<site.config.user.length; i++) {
				if ((this._param.user == site.config.user[i].name) &&
					(this._param.password == site.config.user[i].password)) {
					pverbose("valid "+this._param.user+", "+this._param.password);
					this._user = site.config.user[i];
					login_message = "";
					break;
				}
			}
			
			// first login?
			if (this._user != null) {
				var page = WIKI.search(this._user.name);
				if (page.card == null) {
					WIKI.set({ id: 0, pid: 0, name: this._user.name, wiki: "user's page." });
				}
			}
			
		} else {
			pverbose(_func_+": user name in cookie");
			// has username in cookie
			for(var i=0; i<site.config.user.length; i++) {
				if (this._cookies.user == site.config.user[i].name) {
					this._user = site.config.user[i];
					break;
				}
			}
			has_cookie = true;
		}
		if (this._user == null) {
			// no user information
			pwarning(_func_+": failure");
			return false;
		}
		
		// check expire date
		if (site.config.session.expire != 0) {
			if (typeof(this._cookies.expire) == "undefined") {
				// not login
				return false;
			}
			
			// expires?
			if (Date.now() > this._cookies.expire) {
				return false;
			}
		}
		
		// set/update cookie
		this._res.cookie("user", this._user.name);
		if (site.config.session.expire != 0) {
			res.cookie('expire', Date.now() + 900000);
		}
		
		return true;
	};
}

////////////////////////////////////////
// 
function Session(action)
{
	return function(req, res, next) {
		var context = new Context(req, res, next);
		if (context.validation() == true) {
			pverbose("Session.start: by "+context._user.name);
			try {
				print("========== session "+req.url);
				action(context);
			} catch(e) {
				panic("detecting an exception...");
				panic("file: "+e.fileName);
				panic("line: "+e.lineNumber);
				panic(e);
			}
		} else {
			res.redirect("/login");
		}
	};
}

////////////////////////////////////////
// 
exports.main = function(app, io, uploader)
{
	config.env.app = app;
	config.env.io  = io;
	config.env.uploader = uploader;

	// database
	// diary
	DIARY.initialize();
	app.post("/data/diary/:year/:month/:day", Session(Diary.set_diary));
	app.get("/data/diary/:year/:month/:day", Session(Diary.get_diary));
	app.get("/data/diary/:year/:month", Session(Diary.get_days));
	
	// reference
	REF.initialize();
	
	// wiki
	WIKI.initialize();
	app.post("/data/wiki/search", Session(Wiki.search));
	app.get("/data/wiki/:id/history", Session(Wiki.history));
	app.post("/data/wiki/:id", Session(Wiki.set));
	app.get("/data/wiki/:id", Session(Wiki.get));
	app.get("/data/wiki/", Session(Wiki.list));
	app.post("/data/wiki/:id/upload", Session(Wiki.upload));
	//app.get("/data/wiki/:id/download", Session(Wiki.download));

	// task
	TASK.initialize();
	app.get("/data/task/", Session(Task.list));
	app.get("/data/task/open", Session(Task.list_open));
	app.get("/data/task/closed", Session(Task.list_close));
	app.post("/data/task/:id", Session(Task.set));
	app.get("/data/task/:id", Session(Task.get));
	app.get("/data/task/:id/children", Session(Task.children));
	
	// bucket
	//app.post("/data/bucket/:user/:what", Session(Bucket.put));
	//app.get("/data/bucket/:user/:what", Session(Bucket.get));
	
	// user
	app.get("/data/user/:user", Session(User.get));
	
	
	app.get("/", Session(Home));
	app.get("/login", Login);
	app.get("/logout", Logout);
	app.post("/authentication", Session(Home));
	
	pverbose("initialized.");
}
