// ==UserScript==
// @name LJ-cuts in the Inbox
// @namespace http://murklins.talkoncorners.net
// @description Show LJ-cut links in the Inbox instead of the cut content. Click to expand cuts.
// @include http://www.livejournal.com/inbox*
// ==/UserScript==

// The basis of this code is stolen from afuna's "lj-cut text in the inbox" GM script:
// http://userscripts.org/scripts/show/33061
// She said I could mangle it in this disturbing fashion.

var inboxItems = new Object();
fetchInboxHTML();

function fetchInboxHTML() {
  // get the current inbox html, unaltered by any page JS or GM scripts
  GM_xmlhttpRequest({
    method: "GET",
    url: window.location.href,
    onload: function(details) { 
      var inboxHTML = details.responseText;
      
      var inbox = document.getElementById('all_Body');
      var cutAnchored = document.evaluate(".//td[@class='item']//a[@name = 'cutid1']", inbox, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	  for (var i = 0; i < cutAnchored.snapshotLength; i++) {
        // get the entry content node
        var inboxItem = cutAnchored.snapshotItem(i);
        while (inboxItem != null && inboxItem.className != "InboxItem_Content") {
          inboxItem = inboxItem.parentNode;
        }
        // get the node that holds the entry link
        var inboxHeader = inboxItem;
        while (inboxHeader != null && inboxHeader.nodeName != "SPAN") {
          inboxHeader = inboxHeader.previousSibling;
        }
        if (inboxItem && inboxHeader) {
          var links = inboxHeader.getElementsByTagName("a");
          var entryLink = links[0].href;
          inboxItems[entryLink] = inboxItem;
          
          // get the inbox item id
          var itemTR = inboxItem;
          var inboxEntryHTML = "";
          while (itemTR != null && itemTR.nodeName != "TR") {
            itemTR = itemTR.parentNode;
          }
          var itemID = itemTR.getAttribute("lj_qid");
            
          // get this item's js-free entry content
          var inboxEntryHTML = "";
          var entryRegex = new RegExp("<tr .*? lj_qid=\"" + itemID + "\" [\\s\\S]*?<div class=\"InboxItem_Content\" style=\"display: block;\">([\\s\\S]*?)<div class='actions'>");
          var inboxHTMLArr = entryRegex.exec(inboxHTML);
          if (inboxHTMLArr) {
            inboxEntryHTML = inboxHTMLArr[1];
          }
          fetchCuts(entryLink, inboxEntryHTML);      
        }
      }
    }
  });
}

function fetchCuts(entryLink, inboxEntryHTML) {
  // find date page
  GM_xmlhttpRequest({
    method: "GET",
    url: entryLink +"?mode=reply&format=light",
    onload: function(details) {      
      var journalLink = entryLink.split("/").slice(0,-1).join('/');
      var dateRegex = new RegExp(journalLink+"/\\d{4}/\\d{2}/\\d{2}/");
      var dayLink = details.responseText.match(dateRegex);            
      fetchCutsFromDayPage(dayLink, entryLink, inboxEntryHTML);
    }
  });
}

function fetchCutsFromDayPage(dayLink, entryLink, inboxEntryHTML) { 
  GM_xmlhttpRequest({
    method: "GET",
    url: dayLink+"",
    onload: function(details) {
      // need to pull out cut id and cut text
      var cutRegex = new RegExp(entryLink+"#cutid(\\d+)\">(.*?)</a>","g");
      var cuts = new Object();
      var arr;
      var unCutTextArr;
      var contentWithCuts = "";
      var hasPoll = true;
      
      // Now check for polls -- if there are polls, we'll just show the cut text, and not attempt to 
      // mess with hiding/showing the cut contents because that breaks the polls.                
      var pollRegex = new RegExp("<form action='http://www.livejournal.com/poll/\\?id=(\\d*)' method='post'>", "g");
      if (inboxEntryHTML.match(pollRegex) == null) {   
        hasPoll = false;
      }
      while((arr=cutRegex.exec(details.responseText))) {              
        // Save the cut text
        var cid = Number(arr[1]);
        cuts["cutid" + cid] = arr[2];                 

        if (hasPoll == false) {    
          // Now grab the stuff that comes in between cuts, but is not inside the cut itself
          var betweenCuts = new RegExp(entryLink + "#cutid" + cid + "\">.*?</a>&nbsp;\\)</b>([\\s\\S]*?)<b>\\(&nbsp;<a href=\"" + entryLink + "#cutid" + (cid + 1) + "\">.*?</a>&nbsp;\\)</b>","g");
          unCutTextArr = betweenCuts.exec(details.responseText);
          if (unCutTextArr != null) {
            contentWithCuts = contentWithCuts + " <b style=\"color: #B22222;\">( <a name=\"cutid" + cid + "\" href=\"" + entryLink + "#cutid" + cid + "\" style=\"text-decoration: none; color: #B22222;\">lj-cut: " + arr[2] + "</a> )</b> " + unCutTextArr[1];
          }
          else {
            var afterCut = new RegExp(entryLink + "#cutid" + cid + "\">.*?</a>&nbsp;\\)</b>([\\s\\S]*)", "g");
            var dayHTML = "";
            unCutTextArr = afterCut.exec(details.responseText);
            if (unCutTextArr) {
              dayHTML = unCutTextArr[1];
            }
            // This bit coming up is by far the twitchiest part of the code. ARGH.
           
            // For the last cut in the entry, we need to trim down the day view's post-cut text to remove the html that is not part of
            // the target entry (have to grab it all since day view is style-specific, so no way to know what html indicates the end
            // of the entry content and the beginning of other parts of the layout). There is a chance that some of the layout portion of
            // the day view code will inadvertently match the end of entry content, but as this is likely to be trivial code like trailing
            // div closures and breaks, this chance can probably be taken without dire consequence. 
            
            // When we compare the post-cut text of the inbox entry to the day page html, we actually need to compare, not the inbox html iteself,
            // which has been subjected to some JS mods after loading, but the JS-free entry HTML we got earlier.
            var preCutHTML = "";
            var postCutHTML = "";
            if (inboxEntryHTML) {
              // reduce the entry html to just what comes after the last cut marker
              var postCut = new RegExp("<a name=\"cutid" + cid + "\"></a>([\\s\\S]*)");
              var postCutArr = postCut.exec(inboxEntryHTML);
              if (postCutArr) {
                postCutHTML = postCutArr[1];
              }              
              // while we're here, also grab all the stuff before the first cut                        
              var preCut = new RegExp("([\\s\\S]*)<a name=\"cutid1\"></a>");
              var preCutArr = preCut.exec(inboxEntryHTML);
              if (preCutArr) {
               preCutHTML = preCutArr[1];
              }
            }
            
            // Now the day page html can immediately be trimmed down to the length of the entry's post-cut div html
            // since there is no way the matching section of it can be longer than that.                    
            dayHTML = dayHTML.substring(0, postCutHTML.length);
                                  
            // Match the two segments of HTML to find the overlapping piece that is the suffix of the entry html 
            // and the prefix of the day html.
            postCutHTML = matchEntryText(postCutHTML, dayHTML);
            
            // Stick it all together.                                                        
            contentWithCuts = preCutHTML + contentWithCuts + " <b style=\"color: #B22222;\">( <a name=\"cutid" + cid + "\" href=\"" + entryLink + "#cutid" + cid + "\" style=\"text-decoration: none; color: #B22222;\">lj-cut: " + arr[2] + "</a> )</b> " + postCutHTML;
          }
        }              
      }
      insertCuts(entryLink, cuts, hasPoll, contentWithCuts);
    }
  });
}

// a pattern matching algorithm so naive you might as well go ahead and call it ignorant
function matchEntryText(entryHTML, dayHTML) {
  if (entryHTML != "" && dayHTML != "") {
    var testSize = Math.ceil(dayHTML.length/2);
    var testStr = dayHTML.substring(0, testSize);        
    if (entryHTML.indexOf(testStr) == -1) {
      // no match found, so reduce the test size and try again
      if (testStr.length == 1) {
        // no hope of a match!
        return "";
      }
      return matchEntryText(entryHTML, testStr);
    }
    else {
      // found one or more matches, so go back up to previous size and start looking for a match, char by char
      testStr = dayHTML;
      while (testStr) {
        if (entryHTML.lastIndexOf(testStr) == (entryHTML.length - testStr.length)) {
          // match found
          break;
        }
        // no match, so cut string by one char
        testStr = testStr.substring(0, testStr.length - 1);
      }
      return testStr;
    }
  }
  return "";
}    

function insertCuts(entryLink, cuts, hasPoll, contentWithCuts) {
  var inboxItem = inboxItems[entryLink];
  var anchors = document.evaluate(".//a[contains(@name,'cutid')]", inboxItem, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);    
  for (var i = 0; i < anchors.snapshotLength; i++) {    
    var curNode = anchors.snapshotItem(i);
    var cutid = curNode.name;
    // add in the cut text to the un-cut inbox entry
    var b = document.createElement("b");
    curNode.parentNode.insertBefore(b, curNode);        
    b.appendChild(document.createTextNode(" ( lj-cut text: " + cuts[curNode.name] + " ) "));
  }
  if (hasPoll == false) {
    // Create some new child divs for the inbox item content div. One to hold the new Expand/Hide Cuts link,
    // one to hold the content in its cut form and one to hold the content in its original form. 
    var controlCuts = document.createElement("div");
    var newContentDiv = document.createElement("div");
    var newCutContentDiv = document.createElement("div");
    newContentDiv.style.display = "none";
     
    var contentNode =  inboxItem.firstChild;
    inboxItem.insertBefore(newContentDiv, contentNode);
    inboxItem.insertBefore(newCutContentDiv, newContentDiv);
    inboxItem.insertBefore(controlCuts, newCutContentDiv);
           
    // Now remove all the content nodes and append them to the new div that was created to hold the content
    // in its original inbox form. Basically, we are just adding a level of dom heirarchy between the 
    // inbox item contents div and the actual content nodes.  (Could have regexed this part and shoved it in 
    // via innerHTML like gets done to fill the cut content div, but this seemed safer in terms of working with 
    // other GM scripts. Or maybe I am a lunatic.)
    while (contentNode != null && !(contentNode.className == "actions" && contentNode.nodeName == "DIV")) {
      // separate the current node from its parent and append it to the new content div
      var node = contentNode;
      contentNode = contentNode.nextSibling;
      inboxItem.removeChild(node);
      newContentDiv.appendChild(node);
    }   
     
    // Fill the div holding the content with the cuts that we obtained from the day page
    newCutContentDiv.innerHTML = contentWithCuts;
    newCutContentDiv.style.display = "block";
   
    // Stop the new cut links from selecting/deselectig the item -- they should just take you to the entry page
    var cutLinks = document.evaluate(".//a[contains(@name,'cutid')]", newCutContentDiv, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);    
    for (var i = 0; i < cutLinks.snapshotLength; i++) {    
      var curLink = cutLinks.snapshotItem(i);
      curLink.addEventListener("click", 
        function(event) {
          event.stopPropagation();
        },
        false
      );
    }
     
    // Last bit. Add the link that controls whether the cut or uncut content is displayed.
    var expandHideLink = document.createElement("a");
    controlCuts.appendChild(expandHideLink);
    controlCuts.style.marginBottom = "10px";
    expandHideLink.innerHTML = "Expand Cuts";
    expandHideLink.style.fontWeight = "bold";
    expandHideLink.style.color = "#B22222";
    expandHideLink.addEventListener("click", 
      function(event) {
        event.stopPropagation();
        event.preventDefault();
        // if the controlCuts div's next sibling (the cut version div) is showing, hide it and show the uncut version                                
        var currentCutDisplay = this.parentNode.nextSibling.style.display;
        this.parentNode.nextSibling.style.display = this.parentNode.nextSibling.nextSibling.style.display;
        this.parentNode.nextSibling.nextSibling.style.display = currentCutDisplay;
        
        // also swap the Expand/Hide text
        this.innerHTML = (this.parentNode.nextSibling.style.display == "block") ? "Expand Cuts" : "Hide Cuts";
      },
      false
    );
  }
}