// ==UserScript==
// @name           diff from Google Cache
// @namespace      http://labs.cybozu.co.jp/blog/nakatani/
// @author         Nakatani Shuyo / Cybozu Labs, Inc.
// @include        *
// ==/UserScript==

$=function(id){return document.getElementById(id);}

var CacheDiff = {

init:function(){
	GM_addStyle([
    "#difftext_area{position:absolute;left:5%;width:90%;background-color:#ccc;opacity:0.8;z-index:100;}",
    "#difftext_mes{border:2px solid black;text-align:center;}",
    ".diff_head{width:50%;text-align:center;border:2px solid #fff;background-color:#ccc;}",
    ".diff_text{text-align:left;border:2px solid #fff;background-color:#ccc;}",
    ".diff_ins{background-color:#f80;}",
    ".diff_del{background-color:#ff0;}",
	].join(' '));
	GM_registerMenuCommand("diff from Google Cache",CacheDiff.proc);
},

proc: function(){
  CacheDiff.current_text = document.body.innerHTML.replace(/\s+/g,' ')

  var url = escape(location.href.replace(/^https?:\/\//,''))
  GM_xmlhttpRequest({
    method: 'GET',
    url: "http://www.google.com/search?q=cache:"+url+"&strip=1",
    onload: function(res){
      if(res.status==200){
        var html = res.responseText;
        if (html.match(/<title>cache:/)) {
          alert('Cannot fetch Google cache of this page.');
          return;
        }
        var div = document.createElement("div");
        div.id = 'difftext_area';
        div.style.top = window.innerHeight * 0.05+"px";
        div.style.minHeight = window.innerHeight * 0.9+"px";
        document.body.appendChild(div);

        var mes = document.createElement("div");
        mes.id = "difftext_mes";
        div.appendChild(mes);

        CacheDiff.cached_analyze(html);
      }
    }
  });
},

morpheme_analyze: function(html, callback) {
  // extract_text
  var st = html.replace(/<li[^>]*>\s*<a.+?<\/a>\s*(?=<li)/ig,'').replace(/<li[^>]*>\s*<a.+?<\/a>\s*<\/li>/ig,'').replace(/<!--.*?-->/ig,'').replace(/<script[^>]*>[^<]*<\/script>/ig,'').replace(/<\/?(br|td|p)[^>]*>/g,"\n").replace(/<.+?>/ig, '').replace(/&nbsp;/g, ' ').replace(/  +/g,' ').replace(/^ +/gm, '').replace(/\n+/g, "\n").replace(/&#(\d+);/g, function(s,n){return String.fromCharCode(parseInt(n))}).replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&laquo;/g,"\u00AB").replace(/&raquo;/g,"\u00BB");

  var url = "http://api.jlp.yahoo.co.jp/MAService/V1/parse";
  var data = "appid=cachediff&results=ma&response=surface&sentence="+encodeURIComponent(st);
  GM_xmlhttpRequest({
    method: 'POST',
    headers: {'Content-type': 'application/x-www-form-urlencoded'},
    url: url,
    data: data,
    onload: callback });
  return st;
},

split_words: function(result_xml) {
  var xml = (new DOMParser).parseFromString(result_xml, "application/xml");
  var words = []
  var list = xml.getElementsByTagName('word');
  for(var i=0;i<list.length;i++) {
    words.push( list.item(i).getElementsByTagName('surface').item(0).textContent );
  }
  return words;
},

cached_analyze: function(html) {
  $('difftext_mes').innerHTML = 'parsing cached text...';

  html = html.replace(/\s+/g,' ').replace(/^.*?<hr>/, '');
  if (html.match(/<body[^>]*>/i)) { html = RegExp.rightContext; }
  CacheDiff.cached_text = CacheDiff.morpheme_analyze(html, function(res){
    if(res.status==200){
      CacheDiff.cached_words = CacheDiff.split_words(res.responseText);
      CacheDiff.current_analyze();
    }
  });
},

current_analyze: function() {
  $('difftext_mes').innerHTML = 'parsing current text...';
  CacheDiff.current_text = CacheDiff.morpheme_analyze(CacheDiff.current_text, function(res){
    if(res.status==200){
      CacheDiff.current_words = CacheDiff.split_words(res.responseText);
      CacheDiff.outputdiff();
    }
  });
},

outputdiff: function() {
  $('difftext_mes').innerHTML = 'diff...';
  var a = CacheDiff.cached_words, b = CacheDiff.current_words;
  var result = CacheDiff.diff2(a, b);
  var st = "<tr><td class='diff_head'>cache</td><td class='diff_head'>current</td></tr>"

  var text_a = CacheDiff.cached_text, text_b = CacheDiff.current_text;

  var output = CacheDiff.makedifftext(text_a, result, "delete", "<span class='diff_del'>","</span>");
  st += "<tr><td class='diff_text'>" + output.replace(/\n/g, "<br />") + "</td>";

  output = CacheDiff.makedifftext(text_b, result, "insert", "<span class='diff_ins'>", "</span>");
  st += "<td class='diff_text'>" + output.replace(/\n/g, "<br />") + "</td></tr>";

  var table = document.createElement("table")
  table.innerHTML = st;
  table.width="100%";
  $('difftext_area').appendChild(table);

  $('difftext_mes').innerHTML = 'done.';
},

makedifftext: function(text,result,command,pre,post){
  var output = ""
  for(var i=0;i<result.length;i++) {
    if (result[i].command == command) {
      var idx = text.indexOf(result[i].item);
      output += text.substring(0, idx) + pre + result[i].item + post;
      text = text.substring(idx+result[i].item.length);
    }
  }
  return output + text;
},

diff2:function(a,b){
  if (a.length<=b.length) {
    return CacheDiff.diff_onp(a, b, "delete", "insert");
  } else {
    return CacheDiff.diff_onp(b, a, "insert", "delete");
  }
},

diff_onp: function(a, b, del, ins) {
  var m = a.length, n = b.length;
  var delta = n - m;
  var fp=[],lst=[], path=[]
  var snake = CacheDiff.snake;
  for(var i=-m-1;i<=n+1;i++) { fp[i]=-1; lst[i]=-1; }
  for(var p=0;p<=m;p++) {
    for(var k=-p;k<delta;k++) {
      snake(fp, lst, path, k, a, b, m, n);
    }
    for(var k=delta+p;k>delta;k--) {
      snake(fp, lst, path, k, a, b, m, n);
    }
    snake(fp, lst, path, delta, a, b, m, n);
    if (fp[delta] >= n) {
      var pt = lst[delta];
      list = []
      while (pt>=0) {
        var buf = eval("["+path[pt]+"]");
        list.push([buf[1], buf[2]]);
        pt = buf[0];
      }
      var x0 = 0, y0 = 0;
      var result = [];
      for(var i=list.length-1;i>=0;i--) {
        var x1 = list[i][0], y1 = list[i][1];
        while (x0<x1 || y0<y1) {
          if (y1-x1 > y0-x0) {
            result.push({item:b[y0++], command:ins});
          } else if (y1-x1 < y0-x0) {
            result.push({item:a[x0++], command:del});
          } else {
            result.push({item:a[x0++], command:'common'});
            y0++;
          }
        }
      }
      return result;
    }
  }
},
snake: function(fp, lst, path, k, a, b, m, n) {
  var y = fp[k-1]+1, pre;
  if (y > fp[k+1]) {
    pre = lst[k-1];
  } else {
    y = fp[k+1];
    pre = lst[k+1];
  }
  var x = y - k;
  while (x<m && y<n && a[x]==b[y]) {
    x++;
    y++;
  }
  fp[k] = y;
  pt = path.length
  path[pt] = pre + "," + x + "," + y;
  lst[k] = pt;
},


}
CacheDiff.init();

