// ==UserScript==
// @name           Multicol Google Search 2008-10-23
// @namespace      net.jintrick
// @include        http://www.google.com/search*
// ==/UserScript==




(function(){
var DEBUG = 0;

/****************/
/*** Preferences***/
/****************/ var Preferences = {
"Googleに閲覧履歴を追跡させない": true,
"コンテクスト説明の文字サイズ": "90%",
"アイテムリンクの文字サイズ": "100%",
"検索フォームは必要ない": false,
"別タブ（窓）で開く": false
};

GM_addStyle("\
    .net-jintrick-context .std{ font-size: "+ Preferences["コンテクスト説明の文字サイズ"] +" !important }\
    h2.r{ font-size: "+ Preferences["アイテムリンクの文字サイズ"] +" !important }\
    .net-jintrick-caution{ display:" + (DEBUG? "block":"none") + "}\
");

GM_addStyle("\
	html{ padding: .5em }\
    body{ padding-bottom: 4.5em; overflow-y: scroll }\
    h3{ margin-bottom: 0; padding: 0 }\
    table{ overflow: hidden }\
    button{ font-family: consolas; margin: 0 .3em; height: 2em; -moz-border-radius: 100%; border: 1px solid #b3bbd1; }\
    button[disabled]{ color: silver }\
    button.net-jintrick-big{ font-size: 150%; width: 3em; height: 3em }\
    .net-jintrick-caution{ color: red; }\
    .net-jintrick-navigation{ text-align: center; position: fixed; left: 0; bottom: 0; width: 100%; background: #c4cce2; border-top: 1px solid blue; opacity: 0.9; z-index: 1000; }\
    .net-jintrick-navigation strong{ font-size: 150%; }\
    form{ margin: 0; padding: 0;}\
    div.g{ margin: 0; padding-bottom: 2em; }\
    .net-jintrick-context{ padding:0; line-height: 1.3; }\
    #net-jintrick-result{ -moz-column-width: 26em; -moz-column-gap: 1em; padding: 0 0 2em }\
");

var d = document;
/*****************/
/*** History hack ***/
/*****************/
function History(){} // 現状 History = {} でも同じ / 履歴をストックするオブジェクト
unsafeWindow._goHist = function(arg){ // echo.html内のGoogle側のスクリプトを乗っ取る
    if (arg == Google.navigation.getPosition())
        return;
    var g, start = Number(arg) * 10 - 10;
    (g = History[start]) ? Google.build(g) : d.body.id = start;
    // id属性変更のイベントによってGM_xmlhttpRequestを実行
};
d.body.addEventListener("DOMAttrModified", function(evt){
    var node = evt.target;
    if (node.tagName == "BODY")
        GM_xmlhttpRequest({method: "GET", url: Google.getURL(node.id), onload: function(res){
            Google.build(new Google(res.responseText));
        }});
}, true);

/*****************/
/*** Define Tools ***/
/*****************/

Function.prototype.xcall = DEBUG?
	function(args){
		try { return this.call(args); }
		catch(err) { alert(arguments.callee.caller); return null; }
	} : Function.prototype.call;

function $(){
    try {
    if (!arguments[1]) {
        var id = arguments[0];
        var e = d.getElementById(id);
        return e;
    } else {
        var contextNode = arguments[0], locationPath = arguments[1];
        var nl = d.evaluate(
            locationPath,
            contextNode,
            d.createNSResolver(contextNode),
            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
            null
        );
        var a=[],i=nl.snapshotLength;
        while(i--) a.push(nl.snapshotItem(i));
        return a;
    }
    } catch(err) {}
}

function $E(tagName, attrs, text){
    var e = d.createElement(tagName);
    if (attrs)
        for (var att in attrs)
            e.setAttribute(att, attrs[att]);
    if (text)
        e.appendChild($T(text));
    return e;
}
function $T(str){
    return d.createTextNode(str);
}

function $R(){
    this.$ = d.createRange();
}
$R.$ = d.createRange();
$R.define = function(name, func){
    $R[name] = $R.prototype[name] = func;
};
$R.define("select", function(node){
            this.$.selectNode(node);
    });
$R.define("selectContents", function(node){ this.$.selectNodeContents(node); });
$R.define("surround", function(element){
        if (typeof(element) == "string")
            element = $E(element);
        this.$.surroundContents(element);
    });
$R.define("extract", function(){ return this.$.extractContents(); });
$R.define("del", function(){ this.$.deleteContents(); });
$R.define("insert", function(node){ this.$.insertNode(node); })
$R.define("castTag", function(element){
        /* elementのタグを除去し内容のみにする。 */
        this.selectContents(element);
        var df = this.extract();
        this.select(element);
        this.del();
        this.insert(df);
    });
$R.define("changeTag", function(newElement, oldElement){
        /* oldElementのタグを変更する。newElementはタグ名もしくは要素 */
        if (typeof(newElement) == "string")
            newElement = $E(newElement);
        this.selectContents(oldElement);
        this.surround(newElement);
        var df = this.extract();
        this.select(oldElement);
        this.del();
        this.insert(df);
    });
$R.define("cloneContents", function(){
        return this.$.cloneContents();
    });

/******************/
/*** Object Google ***/
/******************/
function Google(context){
    if (typeof context == "string") {
        var e = $E("HTML");
        e.innerHTML = context;
        context = e;
    }
    this.context = context;
    this.status = function(){//検索ステータス  例：「ウェブ　　　10件中 1 - 10 件目（0.10秒）」
            return $(context, "descendant::*[@id='resultStats']")[0].cloneNode(true);
        }.xcall();
    var divResult = $(context, "descendant::ol/ancestor::*[@id='ires']")[0];
    
    /* 検索結果アイテムリストの先頭として挿入されるようになったため、取得中止
    this.suggestion = function(){
            // 検索クエリにスペルミスが予想される場合の修正語句の提案
            var p = $E("P", {"id": "net-jintrick-suggestion"});
            try {
                $R.selectContents( $(divResult, "descendant::p[@class='ssp']")[0] );
                p.appendChild( $R.cloneContents() );
            } catch(err) {
                p = document.createDocumentFragment();
            }
            return p;
        }.xcall();
    */
    this.results = function(){
            /* 修正された検索結果の本体部分
            スキーマはDOM Inspectorで観測のこと */
            var div = divResult.cloneNode(true);
            div.id = "net-jintrick-result";
            var nl = $(div, "descendant::h3/a"), a,attrs,i = nl.length; while(i--) { a = nl[i], attrs = {};
                if (Preferences["別タブ（窓）で開く"])
                    a.target = "_blank";
                if (Preferences["Googleに閲覧履歴を追跡させない"])
                    a.removeAttribute("onclick");
            }
            var nl = $(div, "descendant::h3/following-sibling::div"), i = nl.length; while(i--) {
		nl[i].className = "net-jintrick-context";
            }
            return div;
        }.xcall();
    this.relatedQuery = function(){ // 関連検索
            var div = $E("DIV", {"class": "net-jintrick-related-query"});
            try{
                $R.selectContents($(divResult, "preceding-sibling::div[@id='trev']")[0]);
                div.appendChild($R.cloneContents());
            } catch(err) {
                div.appendChild($E("P", {"class": "net-jintrick-caution"}, "GMScript fail to get relatedQuery."));
            }
            div.className = "net-jintrick-related-query";
            return div;
        }.xcall();
    this.position = function(){ // 検索結果全体に対する現在のページ位置（数値） / 参照と同時に履歴への追加も暗黙的に行う
    		try{
            	var position = Number( $(context, "descendant::td[@class='cur']")[0].textContent );
            } catch(err) {
            	var position = 1;
            }
            var start = position*10-10;
            if (!History[start])
                History[start] = this;
            return position;
        }.xcall(this);
}
// Class method
Google.getURL = function(start){
    var L = [], q = Google.queries, loc = location;
    if (start != null)
        q.start = start;
    for (var key in q)
        L.push(key+"="+q[key]);
    return loc.protocol + "//" + loc.host + loc.pathname + "?" + L.join("&");
};
Google.build = function(g){
    g.prefetchNext();
    var c = Google.contents.cloneNode(false);
    var df = d.createDocumentFragment();
    
    // 検索結果リストアイテムの先頭に挿入されるようになったため、取得中止
    // df.appendChild(g.suggestion); 
    
    df.appendChild(g.results);
    df.appendChild(g.status);
    c.appendChild(df);
    d.body.replaceChild(c, Google.contents);
    Google.navigation.refresh(g);
    Google.historyFrame.load(g.position);
    Google.contents = c;
};

// Class field
Google.title = d.title;
Google.queries = function(){
        var dict = {};
        var ret = location.search.slice(1).split("&");
        var prop, i = ret.length; while(i--){
            prop = ret[i].split("=");
            dict[prop[0]] = prop[1];
        }
        return dict;
    }.xcall();
Google.form = function(){
            if(Preferences["検索フォームは必要ない"])
                return $E("SPAN");
            var df = d.createDocumentFragment();
            var form = $("tsf").cloneNode(true);
            df.appendChild(form);
            return df;
        }.xcall();
Google.navigation = function(){
            var self = $E("FORM", {"class": "net-jintrick-navigation"});
            self.addEventListener("submit", function(evt){
                evt.preventDefault();
                self.disableAll();
                self.fire(self.start);
            }, true);
            self.disableAll = function(){
                var nl = $(self, "descendant::button"), i=nl.length; while(i--) nl[i].disabled = true;
            };
            self.setPosition = function(position){ self.position.firstChild.data = position; };
            self.getPosition = function(){ return Number(self.position.textContent); };
            self.lastPosition = 100;
            self.fire = function(start){
                var g;
                if (g = History[start])
                    Google.build(g);
                else
                    GM_xmlhttpRequest({
                        method: "GET", url : Google.getURL(start), onload: function(res){
                            var g = new Google(res.responseText);
                            History[start] = g;
                            Google.build(g);
                        }
                    });
            };
            
            self.refresh = function(g){
                var pos = g.position, lastpos = self.lastPosition;
                var norm = "", big = "net-jintrick-big";
                var first = self.first, back = self.back, next = self.next, last = self.last;
                self.setPosition(pos);
                switch(pos) {
                    case 1:
                        first.disabled = back.disabled = true;
                        next.disabled = last.disabled = false;
                        next.className = big;
                        back.className = norm;
                        Google.reverse = false;
                        break;
                    case lastpos:
                        next.disabled = last.disabled = true;
                        first.disabled = back.disabled = false;
                        next.className = norm;
                        back.className = big;
                        Google.reverse = true;
                        break;
                    default:
                        next.disabled = last.disabled = first.disabled = back.disabled = false;
                        break;
                }
            };
            
                var df = d.createDocumentFragment();
                self.first = function(){
                        var e = $E("BUTTON", {name: "first"}, "|<<");
                        e.addEventListener("click", function(){ self.start = 0; Google.reverse = false; }, true);
                        return df.appendChild(e);
                    }.xcall();
                self.back = function(){
                        var e = $E("BUTTON", {name: "back"}, "<");
                        e.addEventListener("click", function(){ self.start = self.getPosition()*10-20;  }, true);
                        return df.appendChild(e);
                    }.xcall();
                self.position = df.appendChild(
                        $E("STRONG", null, "wait..")
                    ); // $(g.context, "descendant::td[descendant::div[@id='nc']]")[0].textContent
                self.per = df.appendChild($T("/"));
                self.denominator = function(){
                        var g, node = $T("wait...");
                        if (g = History[990])
                            node.data = g.position;
                        else
                            GM_xmlhttpRequest({ method: "GET",
                                url: Google.getURL(990),
                                onload: function(response) {
                                        var lastGoogle = new Google(response.responseText);
                                        var lastPosition = self.lastPosition = lastGoogle.position;
                                        History[990] = History[lastPosition*10-10] = lastGoogle;
                                        node.data = lastPosition;
                                    }
                            });
                        return df.appendChild(node);
                    }.xcall();
                self.next = function(){
                         var e = $E("BUTTON", {name: "next", "class": "net-jintrick-big"}, ">");
                         e.addEventListener("click", function(){ self.start = self.getPosition()*10; }, true);
                         return df.appendChild(e);
                    }.xcall();
                self.last = function(){
                        var e = $E("BUTTON", {name: "last"}, ">>|");
                        e.addEventListener("click", function(){ self.start = 990; Google.reverse = true; }, true);
                        return df.appendChild(e);
                    }.xcall();
            self.appendChild(df);
            return self;
        }.xcall(); // end of Google.navigation
Google.historyFrame = function(){
            var self = $E("IFRAME", {src: "about:blank", style: "display:none;" });
            self.load = function(position){
                self.src = "/calendar/echo.html#"+String(position);
            };
            return self;
        }.xcall();
Google.reverse = false;
// Instance method
Google.prototype.prefetchNext = function(){
    var g, start = Google.reverse? this.position*10 - 20 : this.position*10;
    if (!History[start])
        GM_xmlhttpRequest({ method: "GET", url: Google.getURL(start),
            onload: function(res){
                History[start] = new Google(res.responseText);
            }
        });
};

var google = new Google(d);
var df = d.createDocumentFragment();
df.appendChild(Google.contents = $E("DIV", {id:"net-jintrick-contents"}));
df.appendChild(Google.relatedQuery = google.relatedQuery);
df.appendChild(Google.navigation);
df.appendChild(Google.form);
df.appendChild(Google.historyFrame);
var b = $E("body");
b.appendChild(df);
d.documentElement.replaceChild(b, d.body);
Google.build(google);

})();

