// ==UserScript==
// @name だれでもコメ職人
// @namespace http://www.smartnetwork.co.jp/
// @description コメ職人の真似事をする程度のぐりもん
// @include http://www.nicovideo.jp/watch/*
// ==/UserScript==
//
(function(){
with (D()){

var vid = window.location.pathname.split( "/" ).pop();
var flvdata = {};

//
// 初期処理
//
window.addEventListener( "load", function() {
    // URLチェック。ログインチェック
    if( window.location.href.indexOf( "http://www.nicovideo.jp/watch/" ) != 0 ) return;
    if( document.getElementById( "WATCHFOOTER" ) == null ) return;
    
    var div = document.createElement( "div" );
    div.id = "darecome";
    div.className = "TXT12";
    div.style.display = "none";
    div.innerHTML = '<img id="button_command" src="http://lh6.google.co.jp/syttru/R4mvWB5YaEI/AAAAAAAAAC4/awkK4PJK6Ow/s144/command.png" style="margin-left:16px;vertical-align:top;cursor:pointer;"/><input type="text" id="command" class="TXT12" style="margin-left:4px;width:106px;vertical-align:top;"/><input type="text" id="comment" class="TXT12" style="margin-left:4px;width:245px;vertical-align:top;"/><img id="button_comment" src="http://lh6.google.co.jp/syttru/R4m32B5YaFI/AAAAAAAAADE/8bq4VpunQHk/s144/comment.png" style="margin-left:4px;vertical-align:top;cursor:pointer;"/>　<input type="text" id="min" size="2"/>分<input type="text" id="sec" size="2"/>秒<input type="text" id="msec" size="2"/>';
    
    var div_command = document.createElement( "div" );
    div_command.id = "div_command";
    div_command.className = "TXT12";
    div_command.style.width = "244px";
    div_command.style.height = "300px";
    div_command.style.border = "solid 1px black";
    div_command.style.backgroundColor = "#7edb20";
    div_command.style.display = "none";
    div_command.innerHTML = '<div style="width:100px;float:left;margin:10px;"><div style="width:100px;height:100px;border:solid 1px black;background-color: white; cursor: pointer;" id="command_hishi"><img style="margin-top:12px;" src="http://lh5.google.co.jp/syttru/R4oNIh5YaGI/AAAAAAAAADM/M7O2w7Wv5So/s144/hishi.png"/></div><div style="text-align:center;font-weight:bold;">菱</div></div><div style="width:100px;float:left;margin:10px;"><div style="width:100px;height:100px;border:solid 1px black;background-color: white; cursor: pointer;" id="command_tudumi"><img style="margin-top:12px;" src="http://lh5.google.co.jp/syttru/R4oNIh5YaHI/AAAAAAAAADU/cPoXx8FMguY/s144/tsudumi.png"/></div><div style="text-align:center;font-weight:bold;">鼓</div></div><div style="clear:both;"></div><div style="width:100px;float:left;margin:10px;"><div style="width:100px;height:100px;border:solid 1px black;background-color: white; cursor: pointer;" id="command_yama"><img style="margin-top:12px;" src="http://lh5.google.co.jp/syttru/R4oNIh5YaII/AAAAAAAAADc/O_A_zHh4dbI/s144/yama.png"/></div><div style="text-align:center;font-weight:bold;">山</div></div><div style="width:100px;float:left;margin:10px;"><div style="width:100px;height:100px;border:solid 1px black;background-color: white; cursor: pointer;" id="command_sakazuki"><img style="margin-top:12px;" src="http://lh5.google.co.jp/syttru/R4oNIh5YaJI/AAAAAAAAADk/EsJS08e24SU/s144/sakazuki.png"/></div><div style="text-align:center;font-weight:bold;">杯</div></div><div style="clear:both;"></div>';
    
    var stat = document.createElement( "div" );
    stat.id = "stat";
    stat.innerHTML = "準備中...";
    stat.className = "TXT12";
    stat.style.display = "block";
    
    var footer = document.getElementById( "WATCHFOOTER" );
    footer.parentNode.insertBefore( div, footer );
    footer.parentNode.insertBefore( stat, footer );
    footer.parentNode.insertBefore( div_command, footer );
    
    // コマンドボタンのイベント
    var button_command = document.getElementById( "button_command" );
    button_command.addEventListener( "click", toggle_div_command, false );
    
    // コメントボタンのイベント
    var button_comment = document.getElementById( "button_comment" );
    button_comment.addEventListener( "click", function() {
        var min = document.getElementById( "min" ).value;
        var sec = document.getElementById( "sec" ).value;
        var msec = document.getElementById( "msec" ).value;
        var vpos = "";
        if( ! ( min || sec || msec ) ) {
            return;
        }
        vpos = parseInt( min || 0, 10 ) * 6000 + parseInt( sec || 0, 10 ) * 100 + parseInt( msec || 0, 10 );
        if( isNaN( vpos ) ) {
            return;
        }
        
        var command = document.getElementById( "command" ).value + " 184";
        var comment = document.getElementById( "comment" ).value;
        post( vpos, command, comment );
        document.getElementById( "comment" ).value = "";
    }, false );
    
    // 菱ボタンのイベント
    var command_hishi = document.getElementById( "command_hishi" );
    command_hishi.addEventListener( "mouseover", function(){ this.style.backgroundColor = "cyan" ; }, false );
    command_hishi.addEventListener( "mouseout" , function(){ this.style.backgroundColor = "white"; }, false );
    command_hishi.addEventListener( "click"    , function(){
        document.getElementById( "command" ).value = "菱 虹";
        toggle_div_command();
    }, false );
    
    // 鼓ボタンのイベント
    var command_tudumi = document.getElementById( "command_tudumi" );
    command_tudumi.addEventListener( "mouseover", function(){ this.style.backgroundColor = "cyan" ; }, false );
    command_tudumi.addEventListener( "mouseout" , function(){ this.style.backgroundColor = "white"; }, false );
    command_tudumi.addEventListener( "click"    , function(){
        document.getElementById( "command" ).value = "鼓";
        toggle_div_command();
    }, false );
    
    // 山ボタンのイベント
    var command_yama = document.getElementById( "command_yama" );
    command_yama.addEventListener( "mouseover", function(){ this.style.backgroundColor = "cyan" ; }, false );
    command_yama.addEventListener( "mouseout" , function(){ this.style.backgroundColor = "white"; }, false );
    command_yama.addEventListener( "click"    , function(){
        document.getElementById( "command" ).value = "山";
        toggle_div_command();
    }, false );
    
    // 杯ボタンのイベント
    var command_sakazuki = document.getElementById( "command_sakazuki" );
    command_sakazuki.addEventListener( "mouseover", function(){ this.style.backgroundColor = "cyan" ; }, false );
    command_sakazuki.addEventListener( "mouseout" , function(){ this.style.backgroundColor = "white"; }, false );
    command_sakazuki.addEventListener( "click"    , function(){
        document.getElementById( "command" ).value = "杯 red white red white red";
        toggle_div_command();
    }, false );
    
    
    // 動画情報を取得
    next( function() {
//        GM_log( "動画情報を取得" );
        return xhttp.get( getflvURL( vid ) ).
               next( function( d ) {
                   return parseResponse( d );
               });
    }).
    // チケットをゲットする
    next( function( flv ) {
//        GM_log( "チケットをゲットする" );
        return xhttp.post( flv.ms, ticketData( flv ) ).
               next( function( d ) {
                   return merge( flv, parseTicketResponse( d ) );
               });
    }).
    // postkeyをゲットする
    next( function( flv ) {
//        GM_log( "postkeyをゲットする" );
        return xhttp.get( getpostkeyURL( flv ) ).
               next( function( d ) {
                   return merge( flv, parseResponse( d ) );
               });
    }).
    // 書き込みテスト
    next( function( flv ) { 
//        GM_log( "書き込みテスト" );
        return xhttp.post( flv.ms, commentData( flv, 0, "", " " ) ).
               next( function( d ) { 
                   return merge( flv, parseCommentResponse( d ));
               });
    }).
    // 書き込みテストの結果判定
    next( function( flv ) {
        if( flv.status == 1 ) {
            flvdata = flv;
            document.getElementById( "darecome" ).style.display = "block";
            document.getElementById( "stat" ).style.display = "none";
        }else{
            document.getElementById( "stat" ).innerHTML = "ダメでした。";
        }
    }).
    // エラー処理
    error( function( e ) {
        GM_log( e );
    });
    
}, false );



//
// 書き込み
//
function post( vpos, command, comment ) {
    
    document.getElementById( "stat" ).innerHTML = "書き込み中...";
    document.getElementById( "darecome" ).style.display = "none";
    document.getElementById( "stat" ).style.display = "block";
    
    var d = get_command( command )( vpos, command, comment );
    loop(d.length, function(i) {
        post_comment( flvdata, d[i].vpos, d[i].mail, d[i].comment );
        document.getElementById( "stat" ).innerHTML = "書き込み中... 残り " + (d.length - i);
        return wait( 5 );
    }).
    
    next( function() {
        document.getElementById( "stat" ).style.display = "none";
        document.getElementById( "darecome" ).style.display = "block";
    }).
    
    error( function( e ) {
        GM_log( e );
    });
}



//
// コマンドを取得する
//
function get_command( command ) {
    
    if( command.match( /菱/ ) )
        return hishi;
    
    if( command.match( /鼓/ ) )
        return tsudumi;
    
    if( command.match( /山/ ) )
        return yama;
    
    if( command.match( /杯/ ) )
        return sakazuki;
    
    return normal;
}



//
// 実際の書き込み
//
function post_comment( flv, vpos, mail, comment ) {
    
    // コメント数をゲットする
    next( function() {
        return flv;
    }).
    next( function( flv ) {
//        GM_log( "コメント数をゲットする" );
        return xhttp.post( flv.ms, ticketData( flv ) ).
               next( function( d ) {
                   return merge( flv, parseTicketResponse( d ) );
               });
    }).
    // postkeyをゲットする
    next( function( flv ) {
//        GM_log( "postkeyをゲットする" );
        return xhttp.get( getpostkeyURL( flv ) ).
               next( function( d ) {
                   return merge( flv, parseResponse( d ) );
               });
    }).
    // 書き込み
    next( function( flv ) { 
//        GM_log( "書き込み" );
        return xhttp.post( flv.ms, commentData( flv, vpos, mail, comment ) ).
               next( function( d ) { 
                   return merge( flv, parseCommentResponse( d ));
               });
    }).
    // テストの結果判定
    next( function( flv ) {
        if( flv.status != 0 ) {
            alert( "書き込みが失敗しました。" );
        }
    }).
    // エラー処理
    error( function( e ) {
        alert( e );
    });
    
}



//
// ノーマル
//
function normal( vpos, command, comment ) {
    return [
        {vpos: vpos, mail: command, comment: comment },
    ];
}



//
// 菱型
//
function hishi( vpos, command, comment ) {
    // コメントをバラす
    var chars = comment.split( "", 20 );
    
    // 色
    var colors = parseColor( command );
    
    return [
        {vpos: vpos++, mail: colors[0] + " small", comment: chars.join( "  " ) },
        {vpos: vpos++, mail: colors[1]           , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos++, mail: colors[2]           , comment: chars.join( "　 " ) },
        {vpos: vpos++, mail: colors[3] + " big"  , comment: chars.join( "　" ) },
        {vpos: vpos++, mail: colors[4] + " big"  , comment: chars.join( "　　" ) },
        {vpos: vpos++, mail: colors[3] + " big"  , comment: chars.join( "　" ) },
        {vpos: vpos++, mail: colors[2]           , comment: chars.join( "　 " ) },
        {vpos: vpos++, mail: colors[1]           , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos++, mail: colors[0] + " small", comment: chars.join( "  " ) },
    ];
}




//
// 鼓
//
function tsudumi( vpos, command, comment ) {
    // コメントをバラす
    var chars = comment.split( "", 20 );
    
    // 色
    var colors = parseColor( command );
    
    return [
        {vpos: vpos++, mail: colors[0] + " big"  , comment: chars.join( "　　" ) },
        {vpos: vpos++, mail: colors[1] + " big"  , comment: chars.join( "　" ) },
        {vpos: vpos++, mail: colors[2]           , comment: chars.join( "　 " ) },
        {vpos: vpos++, mail: colors[3]           , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos++, mail: colors[4] + " small", comment: chars.join( "  " ) },
        {vpos: vpos++, mail: colors[3]           , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos++, mail: colors[2]           , comment: chars.join( "　 " ) },
        {vpos: vpos++, mail: colors[1] + " big"  , comment: chars.join( "　" ) },
        {vpos: vpos++, mail: colors[0] + " big"  , comment: chars.join( "　　" ) },
    ];
}



//
// 山
//
function yama( vpos, command, comment ) {
    // コメントをバラす
    var chars = comment.split( "", 20 );
    
    // 色
    var colors = parseColor( command );
    
    return [
        {vpos: vpos      , mail: colors[0] + " ue small", comment: chars.join( "  " ) },
        {vpos: vpos +  40, mail: colors[1] + " ue"      , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos +  80, mail: colors[2] + " ue"      , comment: chars.join( "　 " ) },
        {vpos: vpos + 120, mail: colors[3] + " ue big"  , comment: chars.join( "　" ) },
        {vpos: vpos + 160, mail: colors[4] + " ue big"  , comment: chars.join( "　　" ) },
    ];
}



//
// 杯
//
function sakazuki( vpos, command, comment ) {
    // コメントをバラす
    var chars = comment.split( "", 20 );
    
    // 色
    var colors = parseColor( command );
    
    return [
        {vpos: vpos -   1, mail: "shita big"               , comment: "\u00A0\n\n\n\u00A0\n\n\n\u00A0\n" },
        {vpos: vpos      , mail: colors[0] + " shita small", comment: chars.join( "  " ) },
        {vpos: vpos +  40, mail: colors[1] + " shita"      , comment: chars.join( "  " ) + "\u00A0" },
        {vpos: vpos +  80, mail: colors[2] + " shita"      , comment: chars.join( "　 " ) },
        {vpos: vpos + 120, mail: colors[3] + " shita big"  , comment: chars.join( "　" ) },
        {vpos: vpos + 160, mail: colors[4] + " shita big"  , comment: chars.join( "　　" ) },
    ];
}



// 色
function parseColor( command ) {
    if( command.match( /虹/ ) )
        return ["red", "orange", "yellow", "green", "cyan"];
    
    var colors = getColors( command );
    for( i=0; i<5; i++ ) {
        colors[i] = colors[i] || "";
    }
    return colors;
}


function getColors( str ) {
    var result = [];
    var words = str.split( /[\s　]+/ );
    var colors = {
        red   : 1, pink: 1,
        orange: 1, yellow: 1,
        green : 1, cyan: 1,
        blue  : 1, purple: 1,
        white: 1,
        niconicowhite : 1, white2 : 1,
        truered       : 1, red2   : 1,
        passionorange : 1, orange2: 1,
        madyellow     : 1, yellow2: 1,
        elementalgreen: 1, green2 : 1,
        marineblue    : 1, blue2  : 1,
        nobleviolet   : 1, purple2: 1,
        black:1
    };
    
    for( var i=0; i<words.length; i++ ) {
        if( colors[words[i]] ) {
            result.push( words[i] );
        }
    }
    
    return result;
}



// 「&」と「=」で区切られた文字列を連想配列にする関数
function parseResponseText( str ) {
    var arr = str.split("&");
    var result = {};
    for( var i=0; i<arr.length; i++ ) {
        var kv = arr[i].split( "=" );
        result[kv[0]] = decodeURIComponent( kv[1] );
    }
    return result;
}

// 連想配列を文字列にする（デバッグ用）
function dump( arr ) {
    var result = "";
    for (var property in arr)
        result += property + "=[" + arr[property] + "]";
    return result;
}

// ２つの連想配列を合体させる関数
function merge( destination, source ) {
    for (var property in source)
        destination[property] = source[property];
    return destination;
};

// リクエストの結果を連想配列にする関数
function parseResponse( response ) {
    var result = parseResponseText( response.responseText );
//    GM_log( dump( result ) );
    return result;
}

// getflvのURL
function getflvURL( vid ) {
    var result = "http://www.nicovideo.jp/api/getflv?v=" + vid;
//    GM_log( result );
    return result;
}

// getpostkeyのURL
function getpostkeyURL( flv ) {
    var result = "http://www.nicovideo.jp/api/getpostkey?thread=" + flv.thread_id + "&block_no=" + flv.block_no;
//    GM_log( result );
    return result;
}

// チケット取得リクエストの送信データ
function ticketData( flv ) {
    var result = '<thread res_from="-1" version="20061206" thread="' + flv.thread_id + '" />';
//    GM_log( result );
    return result;
}

// チケット取得リクエストの結果を連想配列にする関数
function parseTicketResponse( response ) {
//    GM_log( response.responseText );
    var ticket = response.responseText.match(/ticket=\"([^\"]*)\"/)[1];
    var last_res = response.responseText.match(/last_res=\"(\d+)\"/)[1];
    var block_no = Math.floor( ( parseInt( last_res ) + 1 ) / 100).toString();
    var result = {ticket: ticket, block_no: block_no};
//    GM_log( dump( result ) );
    return result;
}

// 書き込みリクエストの送信データ
function commentData( flv, vpos, mail, comment ) {
    var result = '<chat premium="' + flv.is_premium + '" ' +
                       'postkey="' + flv.postkey + '" ' +
                       'user_id="' + flv.user_id + '" ' +
                       'ticket="' + flv.ticket + '" ' +
                       'mail="' + mail + ' 184" ' +
                       'vpos="' + vpos + '" ' +
                       'thread="' + flv.thread_id + '">' + comment + '</chat>';
//    GM_log( result );
    return result;
}

// 書き込みリクエストの結果を連想配列にする関数
function parseCommentResponse( response ) {
//    GM_log( response.responseText );
    var status = response.responseText.match( /status=\"(\d+)\"/ )[1];
    var result = {status: status};
//    GM_log( dump( result ) );
    return result;
}



//
// コマンド欄を出したり隠したり
//
function toggle_div_command() {
    var div = document.getElementById( "div_command" );
    div.style.display = div.style.display == "none" ? "block" : "none";
}

}



// Usage:: with (D()) { your code }
// JSDefeered 0.2.1 (c) Copyright (c) 2007 cho45 ( www.lowreal.net )
// See http://coderepos.org/share/wiki/JSDeferred
function D () {


function Deferred () { return (this instanceof Deferred) ? this.init(this) : new Deferred() }
Deferred.prototype = {
	init : function () {
		this._next    = null;
		this.callback = {
			ok: function (x) { return x },
			ng: function (x) { throw  x }
		};
		return this;
	},

	next  : function (fun) { return this._post("ok", fun) },
	error : function (fun) { return this._post("ng", fun) },
	call  : function (val) { return this._fire("ok", val) },
	fail  : function (err) { return this._fire("ng", err) },

	cancel : function () {
		(this.canceller || function () {})();
		return this.init();
	},

	_post : function (okng, fun) {
		this._next =  new Deferred();
		this._next.callback[okng] = fun;
		return this._next;
	},

	_fire : function (okng, value) {
		var self = this, next = "ok";
		try {
			value = self.callback[okng].call(self, value);
		} catch (e) {
			next  = "ng";
			value = e;
		}
		if (value instanceof Deferred) {
			value._next = self._next;
		} else {
			if (self._next) self._next._fire(next, value);
		}
		return this;
	}
};

Deferred.parallel = function (dl) {
	var ret = new Deferred(), values = {}, num = 0;
	for (var i in dl) if (dl.hasOwnProperty(i)) {
		(function (d, i) {
			d.next(function (v) {
				values[i] = v;
				if (--num <= 0) {
					if (dl instanceof Array) {
						values.length = dl.length;
						values = Array.prototype.slice.call(values, 0);
					}
					ret.call(values);
				}
			}).error(function (e) {
				ret.fail(e);
			});
			num++;
		})(dl[i], i);
	}
	if (!num) Deferred.next(function () { ret.call() });
	ret.canceller = function () {
		for (var i in dl) if (dl.hasOwnProperty(i)) {
			dl[i].cancel();
		}
	};
	return ret;
};

Deferred.wait = function (n) {
	var d = new Deferred(), t = new Date();
	var id = setTimeout(function () {
		clearTimeout(id);
		d.call((new Date).getTime() - t.getTime());
	}, n * 1000)
	d.canceller   = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
};

Deferred.next = function (fun) {
	var d = new Deferred();
	var id = setTimeout(function () { clearTimeout(id); d.call() }, 0);
	if (fun) d.callback.ok = fun;
	d.canceller   = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
};

Deferred.call = function (f, args) {
	args = Array.prototype.slice.call(arguments);
	f    = args.shift();
	return Deferred.next(function () {
		return f.apply(this, args);
	});
};

Deferred.loop = function (n, fun) {
	var o = {
		begin : n.begin || 0,
		end   : n.end   || (n - 1),
		step  : n.step  || 1,
		last  : false,
		prev  : null
	};
	var ret, step = o.step;
	return Deferred.next(function () {
		function _loop (i) {
			if (i <= o.end) {
				if ((i + step) > o.end) {
					o.last = true;
					o.step = o.end - i + 1;
				}
				o.prev = ret;
				ret = fun.call(this, i, o);
				if (ret instanceof Deferred) {
					return ret.next(function (r) {
						ret = r;
						return Deferred.call(_loop, i + step);
					});
				} else {
					return Deferred.call(_loop, i + step);
				}
			} else {
				return ret;
			}
		}
		return Deferred.call(_loop, o.begin);
	});
};

Deferred.register = function (name, fun) {
	this.prototype[name] = function () {
		return this.next(Deferred.wrap(fun).apply(null, arguments));
	};
};

Deferred.wrap = function (dfun) {
	return function () {
		var a = arguments;
		return function () {
			return dfun.apply(null, a);
		};
	};
};

Deferred.register("loop", Deferred.loop);
Deferred.register("wait", Deferred.wait);

Deferred.define = function (obj, list) {
	if (!list) list = ["parallel", "wait", "next", "call", "loop"];
	if (!obj)  obj  = (function () { return this })();
	for (var i = 0; i < list.length; i++) {
		var n = list[i];
		obj[n] = Deferred[n];
	}
	return Deferred;
};



function xhttp (opts) {
	var d = Deferred();
	if (opts.onload)  d = d.next(opts.onload);
	if (opts.onerror) d = d.error(opts.onerror);
	opts.onload = function (res) {
		d.call(res);
	};
	opts.onerror = function (res) {
		d.fail(res);
	};
	GM_xmlhttpRequest(opts);
	return d;
}
xhttp.get  = function (url)       { return xhttp({method:"get",  url:url}) }
xhttp.post = function (url, data) { return xhttp({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) }


function http (opts) {
	var d = Deferred();
	var req = new XMLHttpRequest();
	req.open(opts.method, opts.url, true);
	if (opts.headers) {
		for (var k in opts.headers) if (opts.headers.hasOwnProperty(k)) {
			req.setRequestHeader(k, opts.headers[k]);
		}
	}
	req.onreadystatechange = function () {
		if (req.readyState == 4) d.call(req);
	};
	req.send(opts.data || null);
	d.xhr = req;
	return d;
}
http.get  = function (url)       { return http({method:"get",  url:url}) }
http.post = function (url, data) { return http({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) }

Deferred.Deferred = Deferred;
Deferred.http     = http;
Deferred.xhttp    = xhttp;
return Deferred;
}// End of JSDeferred

})();


