/**
 * 今北さん関連スクリプト。
 * prototype.js 1.5.0, script.aculo.us 1.7.0、RickDOM 1.0が必要。
 * @author 高梨ギンペイ（マラカスの人）
 */



/***************************************************************
    グローバル
***************************************************************/

var version = 'imakita201';

/**
 *  ツリーの罫線で使う文字列。
 */
var pathLine = {
    youngest: '　　 ',
    uncle:    '　┃',
    child:    '　┣',
    cadet:    '　┗'
};

/**
 * HTMLで利用されるIDのリスト
 */
var ids = {
    current: 'current',       // 「現在演奏中のひと」の外枠
    thread: {
	panel: 'thread',        // スレッド表示部分
	link:  'thread_link',   // 現行スレへのリンク
	input: 'thread_input'   // 現行スレのアドレス入力フォーム
    },
    tree:    'tree',          // ツリー本体
    independenceSpot: 'independenceSpot', // ノードを落とすとルートへ移動するスポット
    controll: {
	panel: 'controllPanel', // パネル
	open:    'open',        // 編集開始ボタン
	save:    'save',        // 情報保存ボタン
	load:    'load',        // 情報読込ボタン
	test:    'test',        // 保存ボタンをトグル
	copy:    'copy',        // コピーボタン
	loading: 'loagingIcon', // ロード中を示すアイコン
	status:  'status'       // 処理状況
    },
    recents: {
        box:  'recents',       // 「最近の奏者さん」の外枠
	form: 'recents_form',  // 履歴フォーム
	list: 'recents_list'   // 履歴フォーム中の奏者さんリスト
    },
    edit: {
	box:        'edit',            // 「奏者さん情報編集」の外枠
        form:       'edit_form',       // 編集フォーム
	icon:       'edit_icon',       // 編集フォーム中のアイコン選択域
	iconImg:    'edit_iconImg',    // 編集フォーム中のアイコンのプレビュー
	name:       'edit_name',       // 編集フォーム中の名前入力域
	alias:      'edit_alias',      // 編集フォーム中の表示名入力域
	instrument: 'edit_instrument', // 編集フォーム中の演奏パート入力域
	url:        'edit_url',        // 編集フォーム中のアドレス入力域
	st: {
	    kicked: 'edit_st_kicked', // 編集フォーム中のアク禁中チェックボックス
	    video:  'edit_st_video'   // 編集フォーム中の動画チェックボックス
	}
    }
}; // var ids

/**
 * HTMLで利用されるクラスのリスト
 */
var classes = {
    instIcon: 'instIcon',
    hover:    'hovering',
    node: {
	frame:      'nodes',
	path:       'node_path',
	icon:       'node_icon',
	player:     'node_player',
	alias:      'node_name',
	url:        'node_url',
	instrument: 'node_instrument',
	st: {
	    frame:  'node_st',
	    kicked: 'node_st_kicked',
	    video:  'node_st_video'
	}
    }
}; // var classes

/**
 * HTMLのノード。SetObj()で初期化。
 */
var obj = {
    current:       null,
    thread: {
	panel:  null,
	link:   null,
	input:  null
    },
    tree:          null,
    independenceSpot: null,
    controllPanel: null,
    status:        null,
    loading:       null,
    recents: {
	form: null,
	list: null
    },
    edit: {
	icon:       null,
	name:       null,
	alias:      null,
	instrument: null,
	url:        null,
	st: {
	    kicked: null,
	    video:  null
	}
    }
}; // var obj
/**
 * [BETA] objを初期化
 */
 // todo: 削除しよう、これ
function SetObj() {
    obj.current       = MakeRick($(ids.current));
    obj.thread.panel  = MakeRick($(ids.thread.panel));
    obj.thread.link   = MakeRick($(ids.thread.link));
    obj.tree          = MakeRick($(ids.tree));
    obj.tree.extend(ImakitaTreeMethods);

    obj.controllPanel = MakeRick($(ids.controll.panel));
    // obj.recents       = MakeRick($(ids.recents.list));
    // obj.recents.extend(ImakitaRecentsMethods);
}



/***************************************************************
    HTML要素に割り当てるメソッド群
***************************************************************/
/**
 * コントロール オブジェクトに割り当てたりするメソッド群。
 * 特に親オブジェクトの不要なメソッドもここで定義する。
 */
var ImakitaControllMethods = {
    /**
     * 編集開始ボタンを押したときの処理。
     */
    open: function(event) {
	SetObj(); // objを初期化

	// 現行スレのリンクを入力域に
	var url = obj.thread.link.href; // 先にURLを取得しておく
	obj.thread.panel.removeChild(obj.thread.link);
	obj.thread.panel.create('input', {id: ids.thread.input}, {type: 'text', value: url});

	// ツリーを空にする
	obj.tree.setText('ちょっと待ってね ⊂二二二（　＾ω＾）二⊃ﾌﾞｰﾝ');

	// ノードを落とすとルートへ移動させるスポットを作る
	var independenceSpot = new Rick('div', {id: ids.independenceSpot, text: '　'});
	obj.current.get('.content')[0].insertBefore(independenceSpot, obj.controllPanel);

	// ノードを落とすとルートへ移動させるスポットのオブジェクトを設定する
	obj.independenceSpot = MakeRick($(ids.independenceSpot));

	obj.independenceSpot.show = ImakitaControllMethods.independenceSpot_show;
	obj.independenceSpot.hide = ImakitaControllMethods.independenceSpot_hide;
	Droppables.add(obj.independenceSpot, {
	    hoverclass: classes.hover,
	    accept: classes.node.player,
	    onDrop: ImakitaControllMethods.independenceSpot_ondropped
	});

	// コントロールパネルを編集モードに
	obj.controllPanel.removeAll();
	obj.controllPanel.add('アイコンをドラッグしてツリーを編集して下さい。他の奏者さんにドロップでぶら下がり、「最近の奏者さん」「奏者さん情報編集」ボックスにドロップで終了、編集できます。');
	obj.controllPanel.create('br');
	obj.controllPanel.create('input', {id: ids.controll.save}, {type: 'button', value: '保存'});
	obj.controllPanel.create('input', {id: ids.controll.load}, {type: 'button', value: '読込'});
	obj.controllPanel.create('input', {id: ids.controll.test}, {type: 'button', value: 'ヽ(・∀・)ノ', title: 'ツリーをいじって遊ぶよ！'});
	obj.controllPanel.create('input', {id: ids.controll.copy}, {type: 'button', value: 'コピー'});
	obj.controllPanel.create('br');
	obj.controllPanel.add('状況 : ');
	obj.controllPanel.create('img', {id: ids.controll.loading}, {src: 'resources/circle-ball-dark-antialiased.gif', width: 16, height: 16, alt: ''});
	obj.controllPanel.create('span', {id: ids.controll.status, text: '準備中...'});

	// コントロールパネルのオブジェクトを設定
	obj.save    = MakeRick($(ids.controll.save));
	obj.load    = MakeRick($(ids.controll.load));
	obj.test    = MakeRick($(ids.controll.test));
	obj.copy    = MakeRick($(ids.controll.copy));
	obj.status  = MakeRick($(ids.controll.status));
	obj.loading = MakeRick($(ids.controll.loading));

	obj.save.observe('click', ImakitaControllMethods.save);
	obj.save.able = true; // トグルのフラグ
	obj.load.observe('click', ImakitaControllMethods.load);
	obj.test.observe('click', ImakitaControllMethods.test);
	obj.copy.observe('click', ImakitaControllMethods.copy);
	obj.status.change = ImakitaControllMethods.status_change;

	if(!clipbrd) {
	    obj.copy.disable();
	}
	// working:

	// 履歴のオブジェクトを設定
	obj.recents.box  = MakeRick($(ids.recents.box));
	obj.recents.form = MakeRick($(ids.recents.form));
	obj.recents.list = MakeRick($(ids.recents.list));

	MakeRick(obj.recents).extend(ImakitaRecentsMethods);

	obj.recents.form.observe('submit', ImakitaControllMethods.recents_submit);
	obj.recents.list.observe('dblclick', ImakitaControllMethods.recents_ondblclicked);

	Droppables.add(obj.recents.box, {
	    hoverclass: classes.hover,
	    accept: classes.node.player,
	    onDrop: ImakitaControllMethods.recents_ondropped
	});
	new Draggable(obj.recents.box, {
	    handle: obj.recents.box.get('h2')[0],
	    ghosting: (navigator.userAgent.toLowerCase().indexOf('opera') >= 0) // Operaはz-indexが効かない？場合がある？みたいなのでごまかし
	});

	// 編集ボックスのオブジェクトを設定
	obj.edit.box        = MakeRick($(ids.edit.box));
	obj.edit.form       = MakeRick($(ids.edit.form));
	obj.edit.icon       = MakeRick($(ids.edit.icon));
	obj.edit.iconImg    = MakeRick($(ids.edit.iconImg));
	obj.edit.name       = MakeRick($(ids.edit.name));
	obj.edit.alias      = MakeRick($(ids.edit.alias));
	obj.edit.instrument = MakeRick($(ids.edit.instrument));
	obj.edit.url        = MakeRick($(ids.edit.url));
	obj.edit.st.kicked  = MakeRick($(ids.edit.st.kicked));
	obj.edit.st.video   = MakeRick($(ids.edit.st.video));

	if(navigator.userAgent.indexOf('MSIE') >= 0) { // MSIE6でおかしくなる表示を修正
	    var cn = 'positionLeft_forMSIE'; // temp: classesにまとめるべきか？
	    obj.edit.name.addClassName(cn);
	    obj.edit.alias.addClassName(cn);
	    obj.edit.instrument.addClassName(cn);
	    obj.edit.url.addClassName(cn);
	}

	obj.edit.form.extend(ImakitaEditMethods);

	obj.edit.form.observe('submit', ImakitaControllMethods.edit_submit);
	obj.edit.icon.observe('change', ImakitaControllMethods.icon_onchange);
	obj.edit.name.observe('change', ImakitaControllMethods.name_onchange);

	Droppables.add(obj.edit.box, {
	    hoverclass: classes.hover,
	    accept: classes.node.player,
	    onDrop: ImakitaControllMethods.edit_ondropped
	});
	new Draggable(obj.edit.box, {
	    handle: obj.edit.box.get('h2')[0],
	    ghosting: (navigator.userAgent.toLowerCase().indexOf('opera') >= 0) // Operaはz-indexが効かない？場合がある？みたいなのでごまかし
	});


	// 表示
	obj.recents.box.show();
	obj.edit.box.show();

	// 情報を最新のものに更新
	ImakitaControllMethods.load();
	ImakitaControllMethods.icon_onchange();

	// 状況表示を戻すのはonloadedに任せる

	/* debug: *
	var temp_player = {
	    icon: 'dr',
	    name: 'マッカス',
	    alias: 'マラたん',
	    instrument: 'パーカッション',
	    url: 'http://localhost/vip-gig/imakita/template.html',
	    kicked: false,
	    video: false
	};
	obj.edit.form.setPlayer(temp_player);
	// */
    }, // open

    /**
     * 奏者さん情報から保存用のXML要素ノードを作成し返す。
     * @param player    奏者さん情報。
     */
    createPlayerXML: function(player) {
	var param = { // 属性として保存するものリスト。
	    icon:       player.icon,
	    name:       player.name,
	    alias:      player.alias,
	    instrument: player.instrument,
	    url:        player.url,
	    kicked:     player.kicked,
	    video:      player.video
	};
	return new Rick('div', {className: 'player'}, param);
    },

    /**
     * createPlayerXML()で作成されたノードから奏者さん情報を得る。
     * @param node      createPlayerXML()で作成されたノード。
     * @return  奏者さん情報
     */
    getPlayerFromXML: function(node) {
	var player = {
	    name:       node.getAttribute('name'),
	    alias:      node.getAttribute('alias'),
	    instrument: node.getAttribute('instrument'),
	    icon:       node.getAttribute('icon'),
	    url:        node.getAttribute('url'),
	    kicked:     eval(node.getAttribute('kicked')),
	    finished:   eval(node.getAttribute('finished'))
	};
	return player;
    },

    /**
     * 奏者さんドラッグ開始の処理。
     * @param element   ドラッグ中の要素。
     */
    node_dragStart: function(element) {
	// ルートへ移動させるスポットを可視化
	obj.independenceSpot.setText('ルートへ');
	obj.independenceSpot.show();

	// 以下デフォルト動作
	element._opacity = Element.getOpacity(element);
	Draggable._dragging[element] = true;
	new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
    },

    /**
     * 奏者さんドラッグ終了の処理。
     * @param element   ドラッグ中の要素。
     */
    node_dragEnd: function(element) {
	// ルートへ移動させるスポットを不可視化
	obj.independenceSpot.hide();
	obj.independenceSpot.setText('　');

	// 以下デフォルト動作
 	var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
 	new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
 	    queue: {scope:'_draggable', position:'end'},
 	    afterFinish: function(){ 
 		Draggable._dragging[element] = false 
 	    }
 	});
    },

    /**
     * 奏者さんドラッグ開始の処理。
     * @param child     ドロップした要素。
     * @param parent    ドロップされた要素。
     */
    node_ondropped: function(child, parent) {
	child.parentNode.follow(parent.parentNode);
    },

    /**
     * ノードを落とすとルートへ移動させるスポットを可視化する。
     */
    independenceSpot_show: function() {
	this.addClassName('visible');
    },

    /**
     * ノードを落とすとルートへ移動させるスポットを不可視化する。
     */
    independenceSpot_hide: function() {
	this.removeClassName('visible');
    },

    /**
     * ノードを落とすとルートへ移動させるスポットにノードが落とされたときの処理。
     * @param node      ドロップした要素。
     * @param spot      ドロップされた要素。
     */
    independenceSpot_ondropped: function(node, spot) {
	node.parentNode.setPath('');
	node.parentNode.follow(obj.tree);
	node.parentNode.restructurePath();
    },

    /**
     * 状態表示域を変更する。<code>obj.status.change</code>に割り当てられる。
     * @param text       設定する文字列。
     */
    status_change: function(text) {
	this.setText(text);
    },

    /**
     * 今北ノードのアイコンをダブルクリックしたときの処理。
     * @param event      イベント
     */
    icon_ondblclick: function(event) {
	Event.element(event).parentNode.parentNode.edit();
    },

    /**
     * 保存ボタンを押したときの処理。
     * @param event      イベント
     */
    save: function(event) {
	// todo: load/saveで同じ処理をしている。まとめるべき。
	// 保存中の表示をする
	obj.save.disable();
	obj.load.disable();
	obj.test.disable();
	obj.loading.show();
	obj.status.change('状態を保存中...');

	// 保存するデータをまとめる。
	var data = new Rick('div', {id: 'tmp'});
	data.add(obj.tree.getSaveData());
	data.add(obj.recents.getSaveData());

	// 通信開始
	var postBody = {
	    version: version,
	    url: $F(ids.thread.input),
	    treeHTML: obj.tree.innerHTML,
	    dat: data.innerHTML
	};
	new Ajax.Request('io.php', {
	    method: 'post',
	    postBody: $H(postBody).toQueryString(),
	    onSuccess:  ImakitaControllMethods.save_onsuccess,
	    onFailure:  ImakitaControllMethods.save_onfailure,
	    onComplete: ImakitaControllMethods.save_oncomplete
	});
    },

    /**
     * [TEMP] 読込が成功したときの処理。
     * @param result     結果。
     */
    save_onsuccess: function(result) {
	if(result.responseText.substr(0, 4) != '+OK ') { // todo: リテラル
	    alert('エラー :\n' + result.responseText);
	    obj.status.change('な、なんか変なデータが返ってきました……！　(ﾟдﾟ；)');
	    return;
	}
	obj.status.change('保存完了！　Σd(・∀・)');
    },

    /**
     * 読込が失敗したときの処理。
     * @param result     結果。
     */
    save_onfailure: function(result) {
	alert('なんか失敗しました。 (´・ω・｀)');
	obj.status.change('保存失敗。。　(´・ω・｀)');
    },

    /**
     * 読込が完了したときの処理。
     * @param result     結果。
     */
    save_oncomplete: function(result) {
	// 読込中の表示を戻す
	obj.loading.hide();
	obj.save.enable();
	obj.save.able = true;
	obj.load.enable();
	obj.test.enable();
    },

    /**
     * 読込ボタンを押したときの処理。
     * @param event      イベント
     */
    load: function(event) {
	// 読込中の表示をする
	obj.save.disable();
	obj.load.disable();
	obj.test.disable();
	obj.tree.setText('ちょっと待ってね ⊂二二二（　＾ω＾）二⊃ﾌﾞｰﾝ');
	obj.loading.show();
	obj.status.change('最新の状態に更新中...');
	obj.recents.list.removeAll();

	// 読込開始
	new Ajax.Request('io.php', {
	    method: 'get',
	    parameters: 'version=' + version,
	    onSuccess:  ImakitaControllMethods.load_onsuccess,
	    onFailure:  ImakitaControllMethods.load_onfailure,
	    onComplete: ImakitaControllMethods.load_oncomplete
	});
    }, // load

    /**
     * 読込が成功したときの処理。
     * @param result     結果。
     */
    load_onsuccess: function(result) {
	var response = result.responseText;
	/*
	if(response.substr(0, 4) != '+OK ') { // todo: リテラル
	    alert('エラー :\n' + response);
	    obj.status.change('な、なんか変なデータが返ってきました……！　(ﾟдﾟ；)');
	    return;
	}
	*/
	var dat = new Rick('div');
	dat.innerHTML = response; //.substr(4, response.length);

	// 奏者ツリーを更新
	var tree = dat.get('div#tree')[0];
	obj.tree.removeAll();
	if(!!tree) {
	    obj.tree.init(tree);
	} else {
	    alert('エラー(´・ω・｀)\nなんかツリーの情報が見つかんないです。');
	}

	// 最近の奏者さんリストを更新
	var recents = dat.get('div#recents')[0];
	obj.recents.list.removeAll();
	if(!!recents) {
	    obj.recents.init(recents);
	} else {
	    alert('エラー(´・ω・｀)\nなんか最近の奏者さんリストの情報が見つかんないです。');
	}

	obj.status.change('読込完了！　Σd(・∀・)');
    }, // load_onsuccess

    /**
     * [TEMP] 読込が失敗したときの処理。
     * @param result     結果。
     */
    load_onfailure: function(result) {
	alert('なんか失敗しました。 (´・ω・｀)');
	obj.status.change('読込失敗。。　(´・ω・｀)');
    },

    /**
     * [TEMP] 読込が完了したときの処理。
     * @param result     結果。
     */
    load_oncomplete: function(result) {
	// 読込中の表示を戻す
	obj.loading.hide();
	obj.save.enable();
	obj.save.able = true;
	obj.load.enable();
	obj.test.enable();
    },

    /**
     * ヽ(・∀・)ノボタンを押したときの処理。保存ボタンをトグルする。
     * @param event      イベント
     */
    test: function(event) {
	if(obj.save.able) {
	    obj.save.disable();
	} else {
	    obj.save.enable();
	}
	obj.save.able = !obj.save.able;
    },

    /**
     * コピー ボタンを押したときの処理。ツリーのテキストをクリップボードに転送する。
     * @param event      イベント
     */
    copy: function(event) {
	clipbrd.copyText(obj.tree.toText());
	alert('現在のツリーをコピーしました！\n……た、たぶん。。');
    },

    /**
     * フォームの送信イベントを中止する。
     * @param event     イベント
     * @return  もしかしたらfalseかも。この値をそのままハンドラ(?)の戻り値に使う。
     */
    abortSubmit: function(event) {
	if(event.preventDefault) { // IE以外
	    event.preventDefault();
	} else if(event.returnValue) { // IE7 ?
	    event.returnValue = false;
	}
	// IEとか古いのとか
	return false;
    },

    /**
     * 最近の奏者さんボックスにノードが落とされたときの処理。
     * @param node      ドロップした要素。
     * @param spot      ドロップされた要素。
     */
    recents_ondropped: function(node, spot) {
	node.parentNode.bye();
    },

    /**
     * 履歴フォームのselect要素でダブルクリックされたときの処理。
     * @param event     イベント。
     */
    recents_ondblclicked: function(event) {
	obj.recents.getBackPlayer();
    },

    /**
     * 履歴フォームが送信されたときの処理。実際の送信は中止する。
     * @param event     イベント。
     * @return  もしかしたらfalseかも。
     */
    recents_submit: function(event) {
	obj.recents.getBackPlayer();
	return ImakitaControllMethods.abortSubmit(event);
    },

    /**
     * 編集フォームが送信されたときの処理。実際の送信は中止する。
     * @param event     イベント。
     * @return  もしかしたらfalseかも。
     */
    edit_submit: function(event) {
	var player = {
	    icon:       obj.edit.icon.value,
	    name:       obj.edit.name.value,
	    alias:      obj.edit.alias.value,
	    instrument: obj.edit.instrument.value,
	    url:        obj.edit.url.value,
	    kicked:     obj.edit.st.kicked.checked,
	    video:      obj.edit.st.video.checked
	};
	obj.tree.createPlayer(player);
	obj.edit.form.reset();
	ImakitaControllMethods.icon_onchange();
	return ImakitaControllMethods.abortSubmit(event);
    },

    /**
     * 奏者さん情報編集ボックスにノードが落とされたときの処理。
     * @param node      ドロップした要素。
     * @param spot      ドロップされた要素。
     */
    edit_ondropped: function(node, spot) {
	node.parentNode.edit();
    },

    /**
     * 編集フォームのアイコンの選択が変更されのたとき処理。隣の画像を変更する。
     */
    icon_onchange: function() {
	obj.edit.iconImg.src = 'resources/' + obj.edit.icon.value + '.gif';
    },

    /**
     * 編集フォームの名前が変更されたときの処理。同名の奏者さんが履歴に記録されていれば、情報を持ってくる。
     */
    name_onchange: function() {
	if(obj.edit.name.value == '') {
	    return;
	}
	var node = obj.recents.findPlayer(obj.edit.name.value);
	if(node != null) {
	    obj.edit.form.setPlayer(node.player);
	} else if(obj.edit.alias.value == '') {
	    obj.edit.alias.value = obj.edit.name.value + 'たん'
	}
    },

    /**
     * 今北ノードの状態アイコンを生成する。
     * @param className 生成するアイコンのクラス名。画像等はこれから自動的に定まる。
     * @param isVisible 可視状態。
     * @return  生成された状態アイコン ノード。
     */
    iconNode_create: function(className, isVisible) {
	var fname, caption;
	var alt='';

	// クラス名から他の設定を定める。
	switch(className) {
	case 'kicked':
	    fname   = 'kicked';
	    caption = 'アク禁中 (´・ω・｀)';
	    break;

	case 'video':
	    fname   = 'tv';
	    caption = '動画';
	    break;

	default:
	    return;
	}

	// 例外を除き、alt属性を設定する。
	if(className != 'video') {
	    alt = '（' + caption + '）';
	}

	// 実際の要素を生成する。
	var element = new Rick('img', {className: className}, {src: 'resources/' + fname + '.gif', width: 16, height: 16, alt: alt, title: caption});
	if(isVisible) {
	    $(element).show();
	} else {
	    $(element).hide();
	}

	return element;
    }
}; // ImakitaControllMethods



/**
 * 今北ツリーに割り当てるメソッド群
 */
var ImakitaTreeMethods = {
    /**
     * [TEMP] 読み出した情報でツリーを初期化する。
     * @param players   奏者さん情報の配列
     */
    init: function(players) {
	var getPlayer = ImakitaControllMethods.getPlayerFromXML;
	MakeRickAll(players);

	// 誰もいないなら何もしない。
	if(players.get('div.player').length <= 0) {
	    return;
	}

	var stack = new Array();
	stack.push({parent: this, players: players.get('div.player'), cnt: 0});

	/*
	 * スタックにはその階層の今北ノードと処理済ノード数、親ノードを記録する。
	 * その階層のノードを処理し終えたら、スタックをpushする。
	 * ノードが子ノードを持っていたら、スタックに積んで子ノードを同様に処理する。
	 */
	while(stack.length > 0) {
	    var cur = stack.pop();

	    // 現在の階層、現在の位置にあるデータを取得し、
	    // それから今北ノードを作成、親ノードへ追加する。
	    var dat = cur.players[cur.cnt];
	    var node = this.createPlayer(getPlayer(dat));
	    node.follow(cur.parent);
	    cur.cnt++;

	    // まだ兄弟が残っていればスタックにpushして繰り返しに備える。
	    if(cur.cnt < cur.players.length) {
		stack.push(cur);
	    }

	    // 子ノードを持っているようであれば、それをスタックに積む。
	    var children = dat.get('div.player');
	    if(children.length > 0) {
		stack.push({parent: node, players: children, cnt: 0});
	    }
	}
    }, // init

    /**
     * 新規に奏者さんを作成する。
     * @param player 奏者オブジェクト
     * @return 生成されたノード
     */
    createPlayer: function(player) {
	var node = new Rick('div', {className: classes.node.frame});

	// パス
	var pathNode = new Rick('span', {className: classes.node.path});

	// 奏者さん
	var playerNode = new Rick('span', {className: classes.node.player + (player.video?' video':'')});
	var alias = (player.alias!='' ? player.alias : (player.name!='' ? player.name : '名無しさん')); // temp: リテラル
	playerNode.create('img', {className: [classes.node.icon, classes.instIcon].join(' ')},
			  {src: 'resources/' + player.icon + '.gif', width: 15, height: 15, alt: ''});
	playerNode.create('span', {className: classes.node.alias, text: alias},
			  {title: player.name});
	playerNode.add('　');
	playerNode.create('a', {className: classes.node.url, text: player.url},
		      {href: player.url});
	if(player.instrument != '') {
	    var instrument = player.instrument;
	    if(player.video) {
		instrument += '（動画）';
	    }
	    playerNode.add('　');
	    playerNode.create('span', {className: classes.node.instrument,
						      text: '【' + instrument + '】'});
	}

	if(player.video) {
	    $(playerNode.get('a')[0]).addClassName('video'); // temp: クラス名
	}

	// 付加情報
	var iconNode = new Rick('span', {className: classes.node.st.frame});
	iconNode.add(ImakitaControllMethods.iconNode_create('kicked', player.kicked));
	iconNode.add(ImakitaControllMethods.iconNode_create('video',  player.video));

	// ノードを組み立てる
	node.add(pathNode);
	node.add(playerNode);
	node.add(iconNode);
	node.player = player;

	// 操作メソッドを追加
	node.extend(ImakitaNodeMethods);

	new Draggable(node.getPlayerNode(), {
	    handle: node.getIconNode(),
	    revert: true,
	    ghosting: (navigator.userAgent.toLowerCase().indexOf('opera') >= 0), // Operaはz-indexが効かない？場合がある？みたいなのでごまかし
	    starteffect: ImakitaControllMethods.node_dragStart,
	    endeffect: ImakitaControllMethods.node_dragEnd
	});
	Droppables.add(node.getPlayerNode(), {
	    hoverclass: classes.hover,
	    accept: classes.node.player,
	    onDrop: ImakitaControllMethods.node_ondropped
	});

	node.getIconNode().observe('dblclick', ImakitaControllMethods.icon_ondblclick);

	// 履歴に名前が掲載されていれば、それを削除する。
	obj.recents.removePlayer(player.name);

	// ツリーに既に同名の奏者さんがいれば、それを置換する。
	// いなければ、新規に追加する。
	var own;
	if((own = obj.tree.findPlayer(player.name)) != null) {
	    own.player = player;
	    own.replaceNode(own.get('span.' + classes.node.player)[0], playerNode);
	    own.replaceNode(own.get('span.' + classes.node.st.frame)[0], iconNode);
	    return null;
	} else {
	    this.add(node);
	}

	return node;
    },

    /**
     * ツリー全体を文字列に変換して返す。
     * @return ツリー全体の文字列
     */
    toText: function() {
	var text = "";
	var players = this.getAll('div.' + classes.node.frame);
	for(var i=0; i<players.length; i++) {
	    text += players[i].getText() + '\r\n';
	}
	return text;
    },

    /**
     * 保存用のXMLを作成し返す。
     * @return  XMLの要素ノード。
     */
    getSaveData: function() {
	var tree = new Rick('div', {id: 'tree'});
	var nodes = this.get('div.' + classes.node.frame);
	for(var i=0; i<nodes.length; i++) {
	    tree.add(nodes[i].getPlayerXML());
	}
	return tree;
    },

    /**
     * 名前からノードを検索する。
     * @param name 検索する名前
     * @return  発見したノード。発見できなければnullを返す。
     */
    findPlayer: function(name) {
	if(name == '') {
	    return null;
	}

	var nodes = this.getAll('div.' + classes.node.frame);
	for(var i=0; i<nodes.length; i++) {
	    if(nodes[i].player.name == name) {
		return nodes[i];
	    }
	}
	return null;
    }
}; // ImakitaTreeMethods



/**
 * 今北ノードに割り当てるメソッド群
 */
var ImakitaNodeMethods = {
    /**
     * 子今北ノードを全て取得
     * @return 子今北ノードリスト
     */
    getChildren: function() {
	return this.get('div.' + classes.node.frame);
    },

    /**
     * パスノードを取得
     * @return パスノード
     */
    getPathNode: function() {
	return this.get('span.' + classes.node.path)[0];
    },

    /**
     * パスを取得
     * @return パス文字列
     */
    getPath: function() {
	return this.getPathNode().getText();
    },

    /**
     * パスを設定
     * @param path パス文字列
     */
    setPath: function(path) {
	this.getPathNode().setText(path);
    },

    /**
     * 自分以下のノードのパスを再構築
     */
    restructurePath: function() {
        var element;
        var path;
        var children = this.getChildren();
        var having = children.length;

        // パスを編集
        path = this.getPath()
	    .replace(pathLine.child, pathLine.uncle)
            .replace(pathLine.cadet, pathLine.youngest);

	// 末っ子以外を構築
        for(var i=0; i<having -1; i++) {
            element = children[i];
            element.setPath(path + pathLine.child);
            element.restructurePath();
        }

        // 末っ子を構築
        if(having >= 1) {
            element = children[having -1];
            element.setPath(path + pathLine.cadet);
            element.restructurePath();
        }
    },

    /**
     * 奏者ノードを取得
     * @return 奏者ノード
     */
    getPlayerNode: function() {
	return this.get('span.' + classes.node.player)[0];
    },

    /**
     * 奏者オブジェクトを取得
     * @return 奏者オブジェクト
     */
    getPlayer: function() {
	return this.player;
    },

    /**
     * 奏者オブジェクトから設定
     * @param player 奏者オブジェクト
     */
    setPlayer: function(player) {
    },

    /**
     * アイコンノードを取得
     * @return アイコンノード
     */
    getIconNode: function() {
	return this.getPlayerNode().get('img.' + classes.node.icon)[0];
    },

    /**
     * 子に加わる
     * @param parent 加わる親今北ノード。undefinedならルートへ
     */
    follow: function(parent) {
	// 子孫でないか確認
	// parentから遡っての先祖に自分がいたら、加わらずに終了する。
	var element = parent.parentNode;
	while(element.hasClassName(classes.node.frame)) {
	    if(element == this) {
		return;
	    }
	    element = element.parentNode;
	}

	// 現在の親を記憶しておき、移動後に兄弟ノードのパスを再構築する。
	// もちろん、新しい場所でのパスも再構築する。
	var oldParent = this.parentNode;
	parent.add(this);
	if(oldParent.hasClassName(classes.node.frame)) {
	    oldParent.restructurePath();
	}
	if(parent.hasClassName(classes.node.frame)) {
	    parent.restructurePath();
	}
    },

    /**
     * ツリーから離脱する
     */
    bye: function() {
	// まず現在の子今北ノードを全て親に預ける。
	var parent = this.parentNode;
	var children = this.getChildren();
	for(var i=0; i<children.length; i++) {
	    children[i].follow(parent);
	}

	// いったんルートへ付き、子を預けたノード以下のパスを再構築してから履歴へ追加する。
	this.follow(obj.tree);
	if(parent.hasClassName(classes.node.frame)) {
	    parent.restructurePath();
	} else {
	    for(var i=0; i<children.length; i++) {
		children[i].setPath('');
		children[i].restructurePath();
	    }
	}
	obj.recents.addPlayer(this.getPlayer());

	// やることは全てやったので、心置きなく辞世。
	this.removeMe();
    },

    /**
     * 編集フォームへ内容を転送
     */
    edit: function() {
	obj.edit.form.setPlayer(this.getPlayer());
    },

    /**
     * [BETA] XML
     */
    getPlayerXML: function() {
	var node = ImakitaControllMethods.createPlayerXML(this.player);

	var children = this.getChildren();
	for(var i=0; i<children.length; i++) {
	    node.add(children[i].getPlayerXML());
	}

	return node;
    },

    /**
     */
    getText: function() {
	var text = '';
	var info = this.player;

	// パス
	text += this.getPath();

	// 名前
	text += (info.alias != '' ? info.alias : (info.name != '' ? info.name + 'たん' : '名無しさん'));

	text += '　';

	// アドレス
	text += info.url;

	text += '　';

	// 楽器
	if(info.instrument != '') {
	    text += '【' + info.instrument
		+ (info.video ? '（動画）' : '')
		+ '】';
	}

	// working:
	return text;
    }
}; // ImakitaNodeMethods



/**
 * 最近の奏者さんリストのフォームに加えるメソッド群
 */
var ImakitaRecentsMethods = {
    /**
     * 保存する最大数
     */
    max: 100,

    /**
     * 渡されたリストで内容を初期化する。
     * @param players   奏者さん情報の配列。
     */
    init: function(nodes) {
	var players = MakeRick(nodes).get('div.player');
	var first = (this.max < players.length ? this.max : players.length);

	for(var i=first-1; i>=0; i--) {
	    var player = ImakitaControllMethods.getPlayerFromXML(players[i]);
	    this.addPlayer(player);
	}
    },

    /**
     * 保存用のXMLを作成し返す。
     * @return  XMLの要素ノード。
     */
    getSaveData: function() {
	var recents = new Rick('div', {id: 'recents'});
	var options = obj.recents.list.options;
	for(var i=0; i<options.length; i++) {
	    recents.add(ImakitaControllMethods.createPlayerXML(options[i].player));
	}
	return recents;
    },

    /**
     * 奏者さんを追加する。
     * @param player 奏者さんオブジェクト。
     */
    addPlayer: function(player) {
	if(player.name != '') {
	    var newbie = obj.recents.list.create('option', {text: player.name}, null, 0);
	    newbie.player = player;
	    newbie.observe('dblclick', ImakitaControllMethods.recents_ondblclicked);
	} else {
	    obj.status.change('名無さんは記録に残らないよ。(´∀｀ )');
	}
    },

    /**
     * 選択中の奏者さんをツリーに移動させる。
     */
    getBackPlayer: function() {
	var pos = obj.recents.list.selectedIndex;

	if(pos >= 0) {
	    obj.tree.createPlayer(obj.recents.list.options[pos].player);
	    // 追加時に履歴を確認してくれるので、ここでの削除は不要。
	}
    },

    /**
     * 名前からノードを検索する。
     * @param name 検索する名前
     * @return  発見したノード。発見できなければnullを返す。
     */
    findPlayer: function(name) {
	if(name == '') {
	    return null;
	}

	var nodes = obj.recents.list.options;
	for(var i=0; i<nodes.length; i++) {
	    if(nodes[i].player.name == name) {
		return nodes[i];
	    }
	}
	return null;
    },

    /**
     * 削除
     */
    removePlayer: function(name) {
	var node = this.findPlayer(name);
	if(node != null) {
	    node.removeMe();
	}
    }
};



/**
 * 奏者さん情報編集フォームに追加するメソッド群
 */
var ImakitaEditMethods = {
    /**
     * 奏者さん情報をフォームにセット
     * @param player 奏者さんオブジェクト
     */
    setPlayer: function(player) {
	obj.edit.icon      .value = player.icon;
	obj.edit.name      .value = player.name;
	obj.edit.alias     .value = player.alias;
	obj.edit.instrument.value = player.instrument;
	obj.edit.url       .value = player.url;
	obj.edit.st.kicked.checked = player.kicked;
	obj.edit.st.video .checked = player.video;

	ImakitaControllMethods.icon_onchange();
    },

    /**
     * 名前から履歴を検索し、該当があれば項目を埋める
     */
    fillDataByName: function() {
    }
};



/***************************************************************
    ロード後、最初の処理
***************************************************************/
Element.observe(window, 'load', function() {
    var openButton = MakeRick($(ids.controll.open));
    openButton.observe('click', ImakitaControllMethods.open);
    openButton.show();
});

// todo: Opでスクロールしてドラッグするとついてこない

