// ==UserScript==
// @name           replace-keyword-input-area
// @namespace      http://d.hatena.ne.jp/fumokmm/
// @description    Replace keyword input area to hot keyword list option
// @include        http://h.hatena.ne.jp/*
// @include        http://h.hatena.com/*
// @exclude        http://h.hatena.ne.jp/id/*
// @exclude        http://h.hatena.ne.jp/keyword/*
// @exclude        http://h.hatena.ne.jp/http/*
// @exclude        http://h.hatena.ne.jp/asin/*
// @exclude        http://h.hatena.com/id/*
// @exclude        http://h.hatena.com/keyword/*
// @exclude        http://h.hatena.com/http/*
// @exclude        http://h.hatena.com/asin/*
// @author         fumokmm
// @date           2009-05-24
// @version        0.04
// ==/UserScript==

(function() {

	// --------------------------------------------------------------
	// ユーティリティ定義＆拡張
	function Util(){}
	extension();
	
	// --------------------------------------------------------------
	// 定数定義
	const LABEL_ID_PAGE_ENTRY = '(idページへ投稿)';
	const HTTP_REG = new RegExp('^' + 'http://h\.hatena\.(ne\.jp|com)/http///');
	const ASIN_REG = new RegExp('^' + 'http://h\.hatena\.(ne\.jp|com)/asin/');

	// --------------------------------------------------------------
	// 変数定義

	var entryForm
	var origin
	var keywordSelect

	// --------------------------------------------------------------
	// メイン処理

	/** マネージャ */
	function Manager(){
		// Hot, Recent, History, New
		this.mode        = null;

		this.entryForm   = null;
		this.container   = null;
		this.tab         = null;

		this.elemHot     = null;
		this.elemRecent  = null;
		this.elemHistory = null;
		this.elemNew     = null;
	}
	Manager.ID_KEYWORD_CONTANER = 'id_keyword_container';
	Manager.ID_KEYWORD_TAB      = 'id_keyword_tab';
	Manager.KEY_MODE            = 'mode';
	Manager.prototype = {
		/** 初期化 */
		init : function() {
			// モードを取得
			this.mode = GM_getValue(Manager.KEY_MODE, 'Hot');

			// 画面からNewのエントリフォームを取得して保持する
			var eForm = Util.xpath("//form[@class='entry-form' and @action='/entry' and @method='post']");
			if (eForm) this.entryForm = eForm[0];

			if (this.entryForm) {
				// 新規に投稿を取得して保持
				var eNew = Util.xpath("//input[@type='text' and @name='word']");
				if (eNew) this.elemNew = eNew[0];

				// コンテナを生成
				this.container = Util.$div({
					'@id' : Manager.ID_KEYWORD_CONTANER
				});
				// コンテナを配置
				this.entryForm.replaceChild(this.container, this.elemNew);

				// タブを生成
				this.tab = Util.$div({
					'@id' : Manager.ID_KEYWORD_TAB,
					'_'   : createSelectKind(this)
				});
				// タブを配置
				this.entryForm.insertBefore(this.tab, this.container);

				// 現在の状態で構築する
				this.rebuild(this);
			}
		},
		/** elemHotを遅延ロードする */
		getHot : function() {
			if (this.elemHot) return this.elemHot;
			// elemHotを取得してから返却
			this.elemHot = createKeywordSelect();
			return this.elemHot;
		},
		/** elemRecentを遅延ロードする */
		getRecent : function() {
			if (this.elemRecent) return this.elemRecent;

			// elemRecentを取得してから返却
			var select = Util.$select({
				'@className'    : 'text',
				'@name'         : 'word',
				'#marginBottom' : '2px',
				'_'             : Util.$option({
									'@value' : '',
									'_'      : LABEL_ID_PAGE_ENTRY
								   })
			});

			// 最近のキーワードのリストを取得
			var recentList = Util.xpath("descendant-or-self::div[@class='entry']/div[@class='list-body']/h2[@class='title']");
			var recentLinks = []
			recentList.forEach(function(item) {
				recentLinks.push(getLink(item));
			});
			recentLinks.unique(function(item){ return item.value; }).forEach(function(item){
				var value;
				if (item.value.match(HTTP_REG)) {
					value = item.value.replace(HTTP_REG, 'http://');
				} else if (item.value.match(ASIN_REG)) {
					value = item.value.replace(ASIN_REG, 'asin:');
				} else {
					value = item.label;
				}

				Util.$add(select, Util.$option({
					'@value' : value,
					'_'      : item.label
				}));
			});

			this.elemRecent = select;
			return this.elemRecent;
		},
		/** elemHistoryを遅延ロードする */
		getHistory : function() {
			if (this.elemHistory) return this.elemHistory;
			// elemHistoryを取得してから返却
			return this.elemHotory;
		},
		/** elemNewを遅延ロードする */
		getNew : function() {
			// Newは必ずあるはず。
			return this.elemNew;
		},
		/** 現在の状態で再構築する */
		rebuild : function(manager) {
			// 投稿部の付け替え
			// ・投稿部の要素を削除して
			while (manager.container.childNodes.length > 0) {
				manager.container.removeChild(manager.container.firstChild);
			}
			// ・modeの要素を替わりに追加
			var elem = eval('manager.get' + manager.mode + '(manager.mode)');
			Util.$add(manager.container, elem);

			// selectedの付け替え
			manager.tabSelected();
		},
		/** タブを変更する */
		changeMode : function(manager, mode) {
			// modeを保存する
			manager.mode = mode;
			GM_setValue(Manager.KEY_MODE, mode);
			// 再構築
			manager.rebuild(manager);
		},
		/** 現在のモードをselectedにする */
		tabSelected : function() {
			var liList = Util.xpath("descendant-or-self::li[contains(@id, " + Manager.ID_KEYWORD_TAB + ")]", this.tab);
			var markId = Manager.ID_KEYWORD_TAB + '_' + this.mode;
			liList.forEach(function(li){
				if (li.id == markId) li.className = 'selected';
				else li.removeAttribute('class');
			});
		},
		toString : function() {
			return 'モード[' + this.mode + ']';
		}
	};

	/**
	 * メイン処理
	 */
	var main = function() {
		var manager = new Manager();
		manager.init(); // 初期化
	}

	//メイン処理を実行
	main()

	// --------------------------------------------------------------
	// メイン処理から呼ばれる関数
	function createKeywordSelect() {
		var select = Util.$select({
			'@className'    : 'text',
			'@name'         : 'word',
			'#marginBottom' : '2px',
			'_'             : Util.$option({
								'@value' : '',
								'_'      : LABEL_ID_PAGE_ENTRY
							   })
		});

		var keywordList = Util.xpath("//ul[@class='list-keyword']/li/a");
		keywordList.forEach(function(k){
			var value;
			if (k.href.match(HTTP_REG)) {
				value = k.href.replace(HTTP_REG, 'http://');
			} else if (k.href.match(ASIN_REG)) {
				value = k.href.replace(ASIN_REG, 'asin:');
			} else {
				value = k.text;
			}

			Util.$add(select, Util.$option({
				'@value' : value,
				'_'      : k.text
			}));
		});

		return select;
	}

	function createSelectKind(manager) {
		// スタイルを追加
		var style = <><![CDATA[
			div.keyword-mode-tab {
				margin              : 0px;
				text-align          : left;
			}
			div.keyword-mode-tab ul {
				font-size           : 80%;
				list-style-image    : none;
				list-style-position : outside;
				list-style-type     : none;
				margin              : 4px 0px;
				padding             : 0px;
			}
			div.keyword-mode-tab ul li {
				border              : 1px solid #BBC4CD;
				display             : inline;
				margin              : 0px;
				padding             : 3px;
			}
			div.keyword-mode-tab ul li a {
				color               : #5F7199;
				cursor              : pointer;
				text-decoration     : none;
			}
			div.keyword-mode-tab ul li a:hover {
				text-decoration     : underline;
			}
			div.keyword-mode-tab ul li.selected {
				background-color    : #6C8CA5;
				color               : #FFFFFF;
				font-weight         : bold;
			}
			div.keyword-mode-tab ul li.selected a {
				color               : #FFFFFF;
				text-decoration     : none;
			}
		]]></>;
		GM_addStyle(style);

		// ------------------------------------------------------
		var hotLabel;
		var hotTitle;
		if (isPublicPage()) {
			hotLabel = 'HOT';
			hotTitle = '注目キーワードの一覧から選択して投稿します。';
		} else if (isUserEntriesPage()) {
			hotLabel = 'お気に入り';
			hotTitle = 'お気に入りキーワードの一覧から選択して投稿します。';
		} else if (isUserFavoritesPage()) {
			hotLabel = 'お気に入り';
			hotTitle = 'お気に入りキーワードの一覧から選択して投稿します。';
		}
		var aHot = Util.$a({
			'@title'       : hotTitle,
			'_'            : hotLabel,
		});
		aHot.addEventListener('click', function() {
			if (manager.entryForm) {
				manager.changeMode(manager, 'Hot');
			}
		}, false);
		var liHot = Util.$li({
			'@id' : Manager.ID_KEYWORD_TAB + '_' + 'Hot',
			'_'   : aHot
		});

		// ------------------------------------------------------
		var aRecent = Util.$a({
			'@title' : '最近投稿されたキーワードの一覧から選択して投稿します。',
			'_'      : '最近'
		});
		aRecent.addEventListener('click', function() {
			if (manager.entryForm) {
				manager.changeMode(manager, 'Recent');
			}
		}, false);
		var liRecent = Util.$li({
			'@id' : Manager.ID_KEYWORD_TAB + '_' + 'Recent',
			'_'   : aRecent
		});

		// ------------------------------------------------------
		var aHistory = Util.$a({
			'@title' : '投稿したキーワード履歴一覧から選択して投稿します。',
			'_'      : '履歴'
		});
		aHistory.addEventListener('click', function() {
			if (manager.entryForm) {
				manager.changeMode(manager, 'History');
			}
		}, false);
		var liHistory = Util.$li({
			'@id' : Manager.ID_KEYWORD_TAB + '_' + 'History',
			'_'   : aHistory
		});

		// ------------------------------------------------------
		var aNew = Util.$a({
			'@title' : '新しくキーワードを作って投稿します。既に存在するキーワードを指定すると、そのキーワードへの投稿になります。',
			'_'      : '新しくキーワードを作って投稿'
		});
		aNew.addEventListener('click', function() {
			if (manager.entryForm) {
				manager.changeMode(manager, 'New');
			}
		}, false);
		var liNew = Util.$li({
			'@id' : Manager.ID_KEYWORD_TAB + '_' + 'New',
			'_'   : aNew
		});

		// ------------------------------------------------------
		var div = Util.$div({
			'@className' : 'keyword-mode-tab',
			'_'          : Util.$add(Util.$ul(), liHot, liRecent, liNew)
		});

		return div;
	}

	/** IDページかどうか */
	function isIdPage() {
		return (
			location.pathname.match(/^\/id\/.+$/)
		);
	}

	/** キーワードページかどうか */
	function isKeywordPage() {
		// /keyword/ で始まり、/keyword/ で終わらない
		return (
			location.pathname.match(/^\/keyword\/.+$/) &&
			!location.pathname.match(/^\/keyword\/$/)
		);
	}

	/** URLキーワードページかどうか */
	function isUrlKeywordPage() {
		// /http///で始まる
		return (
			location.pathname.match(/^\/http\/\/\/.+$/)
		);
	}

	/** ASINキーワードページかどうか */
	function isAsinKeywordPage() {
		// /asin/数字 にマッチ
		return (
			location.pathname.match(/^\/asin\/[0-9]+$/)
		);
	}

	/** パブリックページかどうか */
	function isPublicPage() {
		return (
			location.pathname == '/'
		);
	}

	/** アンテナページかどうか */
	function isUserFavoritesPage() {
		// /keyword/ だけで終わらず、/following で終わる
		return (
			!location.pathname.match(/^\/keyword\/$/) &&
			location.pathname.match(/following$/)
		);
	}

	/** ユーザエントリページかどうか */
	function isUserEntriesPage() {
		return (
			!isKeywordPage()       &&
			!isUrlKeywordPage()    &&
			!isAsinKeywordPage()   &&
			!isUserFavoritesPage() &&
			!isIdPage()            &&
			!isPublicPage()
		);
	}

	/**
	 * ノードのハイパーリンク(キーワード)を取得する
	 * @param node ノード
	 */
	function getLink(node) {
		// ===========NOTE===========
		// NodeType 1 : 要素
		// NodeType 3 : テキストノード
		// ===========NOTE===========

		var children = node.childNodes;
		for (var i = 0; i < children.length; i++) {
			if (children[i].nodeType == 1 && children[i].nodeName == 'A') {
				// プロフィール画像などが入るとテキストノードでない場合があるため
				if (children[i].firstChild.nodeType == 3) {
					return {'value': children[i].href, 'label': children[i].firstChild.nodeValue};
				}
			}
		}
	}

	// --------------------------------------------------------------
	// ユーティリティ定義＆拡張
	function extension() {

		// --------------------------------------------------------------
		// by http://yamanoue.sakura.ne.jp/blog/coding/68
		/**
		 * XPathを便利に扱う関数
		 * @param query
		 * @param context
		 */
		Util.xpath = function(query, context) {
			context || (context = document);
			var results = document.evaluate(query, context, null,
				XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
			var nodes = [];
			for (var i = 0; i < results.snapshotLength; i++) {
				nodes.push(results.snapshotItem(i));
			}
			return nodes;
		}

		// --------------------------------------------------------------
		// by http://gihyo.jp/dev/feature/01/greasemonkey/0002?page=2
		/**
		 * document.createElement() +アルファな関数
		 * attrsに指定した属性を設定し，stylesに指定したCSSプロパティを設定する
		 */
		Util._tag = function(tagName, defmap) {
			var tag = document.createElement(tagName);
			if (defmap) {
				for (a in defmap) {
					if (a.length >= 1) {
						switch(a.charAt(0)) {
							case '@' : // 属性
								if (defmap.hasOwnProperty(a)) tag[a.slice(1)] = defmap[a];
								break;
							case '#' : // スタイル
								if (defmap.hasOwnProperty(a)) tag.style[a.slice(1)] = defmap[a];
								break;
							case '_' : // 子要素
								Util.$add(tag, defmap[a])
								break;
						}
					}
				}
			}
			return tag;
		};

		/**
		 * Element#appendChild() +アルファな関数
		 * 第1引数の要素の末尾要素として第2引数以降で指定する要素を追加する
		 * Element.prototypeの関数として定義するほうがスマートになりそうだが，
		 * GreasemonkeyではElement要素を直接扱えないのでこの定義方法をとった。
		 */
		[ 'div', 'p', 'span', 'a', 'img',
		  'table', 'tr', 'th', 'td',
		  'form', 'label', 'input', 'textarea',
		  'select', 'option', 'ol', 'ul', 'li'
		].forEach( function(tagName) {
			var func = function(attrs, styles) {
				return Util._tag(tagName, attrs, styles);
			};
			eval('Util.$' + tagName + ' = func;');
		} );

		/**
		 * document.createTextNode()のエイリアス
		 */
		Util.$text = function(text) {
			return document.createTextNode(text);
		},

		/**
		 * Element#appendChild() +アルファな関数
		 * 第1引数の要素の末尾要素として第2引数以降で指定する要素を追加する
		 * Element.prototypeの関数として定義するほうがスマートになりそうだが，
		 * GreasemonkeyではElement要素を直接扱えないのでこの定義方法をとった。
		 */
		Util.$add = function(parent, children) {
			if (arguments.length < 2) return '';
			for (var i = 1, child; child = arguments[i]; i++) {
				if (!(child instanceof Array)) child = [child];
				for (var j = 0; j < child.length; j++) {
					if (typeof child[j] != 'object') child[j] = Util.$text(child[j] + '');
					parent.appendChild(child[j]);
				}
			}
			return parent;
		}

		// --------------------------------------------------------------
		// 拡張

		/** サイズを返却 */
		Array.prototype.size = function() {
			return this.length;
		}

		/** 最初の要素を返却。なければnullを返却。 */
		Array.prototype.first = function() {
			return this.length == 0 ? null : this[0];
		}

		/** 最後の要素を返却。なければnullを返却。 */
		Array.prototype.last = function() {
			return this.length == 0 ? null : this[this.length - 1];
		}

		/** 重複を除去したリストを新たに生成して返却 */
		Array.prototype.unique = function(clos) {
			clos || (clos = function(item){return item});
			var hash = {};
			for (var i = 0; i < this.length; i++) hash[clos(this[i])] = this[i];
			var newList = [];
			for (key in hash) newList.push(hash[key]);
			return newList;
		}
	}

	// --------------------------------------------------------------

})()
