// ==UserScript==
// @name          replyALL
// @namespace     net.moeffju.dA
// @description	  Add "Reply to all" to notes
// @include       http://my.deviantart.com/notes/*
// ==/UserScript==

// ý 2006 Matthias Bauer <http://moeffju.deviantart.com/>

/*

Version 0.26
============

0.26
 - Don't do double buttons, now with better recognition
0.25
 - Renamed button back to 'Reply to All', since that is what it does
0.24
 - Renamed button to 'Reply'
0.23
 - Disable button if there's only one recipient available
0.22
 - Updated @value check, className and style
0.21
 - Moved button to the left
0.2
 - If the beta "Reply To All" feature is active, replace the button with the shiny, flashing, no-dupes behaviour ;)
0.11
 - Don't insert extra spaces on repeated clicks of the button
 - Make updater work (so, please update to 0.11 manually!)
0.1
 - First release

*/

/*
 * replyALL is ý 2006 Matthias Bauer <http://moeffju.net/>
 * Licensed under the GNU General Public License, version 2 (but no later version!)
 */
var replyALL = {
  // const
  SCRIPT_NAME : 'replyALL',
  SCRIPT_URL : 'http://moeffju.net/dA/hack/js/replyALL/',
  
  UPDATE_URL : 'http://moeffju.net/dA/hack/js/update',
  VERSION : '0.26',
  
  // vars
  FADE_FPS : 30,
  FADE_DURATION : 1500,
  FADE_COLOR : '#FFFFFF',
  FADE_BG : '#FF0000',
  
  // refs
  NoticeHandler : null,
  Fat : null,
  
  // helper functions
  xpath : function (query, contextNode, resultType) {
    if (null == contextNode) contextNode = document;
    if (null == resultType) resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
    return document.evaluate(query, contextNode, null, resultType, null);
  },
  
  // updater code
  checkVersion : function () {
    var last = GM_getValue('versionCheck.lastOldVersion');
    if (last && last == this.VERSION) {
      this.notifyNewVersion();
      return;
    }
    
    var now = Math.floor(new Date().getTime() / 1000);
    var lastCheckTime = GM_getValue('versionCheck.lastCheckTime', now);
    if (lastCheckTime == -1) { GM_setValue('versionCheck.lastCheckTime', lastCheckTime = now); }
    
    if (now < lastCheckTime + 24*60*60) return; // want at least one day between checks
    
    var url = [this.UPDATE_URL, '?name=', escape(this.SCRIPT_NAME), '&version=', escape(this.VERSION), '&t=', lastCheckTime].join('');
    var self = this;
    
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      headers: {
        'User-Agent': [navigator.userAgent, ' Greasemonkey (', this.SCRIPT_NAME, ')'].join(''),
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      onload: function(response) {
        if (response.status == 200) {
          GM_setValue('versionCheck.lastCheckTime', now);
          eval('var v = '+response.responseText);
          
          if (v && (v.v > parseFloat(self.VERSION))) {
            GM_setValue('versionCheck.lastOldVersion', self.VERSION);
            self.notifyNewVersion();
          }
        }
        else if (response.status == 304) {
          // No change
        }
        else {
          // Now with nicer error handling (thanks to arphire)
          eval('var v = ' + response.responseText);
          var msg = '<b>Update Check failed:</b> ';
          if (v && v.e) msg += v.e + " (Code " + response.status + ")";
          else msg += "Error Code " + response.status + " " + response.statusText;
          self.NoticeHandler.displayNotice(
            msg,
            '#ff8080', '#ff0000', 20, 0.001);
        }
      }
    });
  },
  
  notifyNewVersion : function () {
    this.NoticeHandler.displayNotice(
      'A newer version of <b>' + this.SCRIPT_NAME + '</b> is available.<br/>\n'+
      '<a style="display:block;" href="' + this.SCRIPT_URL + '"><b>Click here to update</b></a>\n',
      '#fdff7c', '#ffff00', 150, 0.001);
  },
  
  // init
  init : function () {
    this.checkVersion();
    this.main();
  },
  
  getUsername : function () {
    if (!document.getElementsByName("deviantMETA")) return;
    eval("var dm = " + document.getElementsByName("deviantMETA")[0].getAttribute('content'));
    if (!dm || !dm.username) return;
    
    return dm.username;
  },
  
  // main
  main : function () {
    var self = this; // for use in closures
    
    // make sure we're actually displaying a note (a#note-body exists)
    if (!document.getElementById('note-body')) return;
    
    this.addReplyToAll();
  },
  
  // display the new button / change the old button ;)
  addReplyToAll : function () {
    var self = this; // for use in closures
    
    var footer = this.xpath("//a[@id='note-body']/../div[1]/div[3]", document, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue;
    var target = footer.getElementsByTagName('input')[0];
    var btra;
    
    // is there a button already?
    if (btra = this.xpath("./input[starts-with(@value, 'Reply')]", footer, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue) {
      btra.removeAttribute('onclick');
      btra.type = 'submit'; // button type was text o.O
    }
    else {
      btra = document.createElement('input');
      
      btra.type = 'submit';
      btra.className = 'button aside-left';
      btra.style.width = '90px';
      btra.style.textAlign = 'center';
    
      footer.insertBefore(btra, target);
      footer.insertBefore(document.createTextNode(' '), target);
    }
    
    btra.value = 'Reply to All';
    
    // do we enable?
    var num_recipients = this.xpath("count(//a[@id='note-body']/../div[1]/div[1]/strong[2]/following-sibling::a)", document, XPathResult.ANY_TYPE).numberValue;
    
    btra.disabled = (num_recipients <= 1);
    btra.name = 'replyall';

    btra.addEventListener('click', function(e) { self.replyToAll(); e.preventDefault(); return true; }, true);
  },
  
  replyToAll : function() {
    var recipients_hash = {}; // associative for dupe removal later on
    
    // get own nick
    var me = this.getUsername().toLowerCase();
    
    // get note header
    var hdr = this.xpath("//a[@id='note-body']/../div[1]/div[1]", document, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue;
    
    // get sender
    var sender = this.xpath("./strong[1]/following-sibling::a[1]", hdr, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue.textContent;
    recipients_hash[sender.toLowerCase()] = sender;
    
    // get recipients
    var recipients_it = this.xpath("./strong[2]/following-sibling::a", hdr, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
    var a;

    while (a = recipients_it.iterateNext()) {
      if (a.textContent.toLowerCase() != me)  // don't add myself
        recipients_hash[a.textContent.toLowerCase()] = a.textContent;
    }
    
    // target element
    var target = document.getElementById('noterecipients');
    
    // read nicks already in the box
    var existing = target.value.replace(/ /g,'').split(',');
    
    for (var i = 0; i < existing.length; i++) {
      if (existing[i].toLowerCase() != me)
        recipients_hash[existing[i].toLowerCase()] = existing[i];
    }
    
    // remap to array
    var recipients = [];
    for (var a in recipients_hash) recipients.push(recipients_hash[a]);
    // sort for good measure
    recipients.sort(function(a,b){return a.toLowerCase()>b.toLowerCase()?1:-1;});
    
    // put in box
    target.value = recipients.join(", ");
    
    // and notify user something happened
    this.Fat.fade_element(target, this.FADE_FPS, this.FADE_DURATION, this.FADE_COLOR);
  },
  
};

/*
 * NoticeHandler is ý 2005-2006 Matthias Bauer <http://moeffju.net/>
 * Licensed under the GNU General Public License, version 2 (and no later version)
 */
var NoticeHandler = {

  VERSION : 0.31,

  fadeNotice : function (elem, delay, step) {
    if (!delay) delay = 50;
    if (!step) step = '.01';
    
    step = parseFloat(step);
    
    if (!elem.style.opacity) {
      elem.style.opacity = '.99999999'; // flicker fix for firefox (admittedly an abhorrent alliteration)
    } else {
      elem.style.opacity = Math.max(parseFloat(elem.style.opacity) - step, 0);
    }
    
    if (parseFloat(elem.style.opacity) > 0.01) {
      var self = this;
      setTimeout(function() { self.fadeNotice(elem, delay, step); }, delay);
    } else {
      var i;
      
      for (i = 0; i < unsafeWindow.notices.length; i++) {
        if (unsafeWindow.notices[i] == elem) {
          unsafeWindow.notices.splice(i, 1);
          setTimeout(function(){ document.body.removeChild(elem); }, 1);
          break;
        }
      }
      
      for (; i < unsafeWindow.notices.length; i++) {
        unsafeWindow.notices[i].style.top = 10 + (i * 50) + 'px';
      }
    }
  },
  
  displayNotice : function (content, bgcol, bordercol, fadeDelay, fadeStep) {
    if (!bgcol) bgcol = '#fdff7c';
    if (!bordercol) bordercol = '#ffff00';
    if (!fadeDelay) fadeDelay = 10;
    if (!fadeStep) fadeStep = 0.001;
    
    var notice = document.createElement('div');
    
    if (!unsafeWindow.notices) unsafeWindow.notices = [];
    unsafeWindow.notices.push(notice);
    
    notice.style.position = 'fixed';
    notice.style.right = '10px';
    notice.style.top = 10 + ((unsafeWindow.notices.length-1) * 50) + 'px';
    notice.style.minWidth = '400px';
    notice.style.height = '30px';
    notice.style.padding = '3px 6px';
    notice.style.border = '2px solid';
    notice.style.textAlign = 'center';
    
    notice.style.backgroundColor = bgcol;
    notice.style.borderColor = bordercol;
    
    notice.innerHTML = content;
    
    this.fadeNotice(notice, fadeDelay, fadeStep, true);
    
    document.body.appendChild(notice);
    
    return notice;
  },
  
};

/*
 * Fat (Fade Anything Technique) is ý 2005 Adam Michela <http://www.axentric.com/posts/default/7>
 * Licensed under Creative Commons Share-Alike 2.0
 *
 * Changes ý 2005-2006 Matthias Bauer <http://moeffju.net/>
 */
var Fat = {
  make_hex : function (r,g,b) 
  {
    r = r.toString(16); if (r.length == 1) r = '0' + r;
    g = g.toString(16); if (g.length == 1) g = '0' + g;
    b = b.toString(16); if (b.length == 1) b = '0' + b;
    return "#" + r + g + b;
  },
  
  fade_element : function (elem, fps, duration, from, to) 
  {
    if (!fps) fps = 30;
    if (!duration) duration = 3000;
    if (!from || from=="#") from = "#FFFF33";
    if (!to) to = this.get_bgcolor(elem);
    
    var frames = Math.round(fps * (duration / 1000));
    var interval = duration / frames;
    var delay = interval;
    var frame = 0;
    
    if (from.length < 7) from += from.substr(1,3);
    if (to.length < 7) to += to.substr(1,3);
    
    var rf = parseInt(from.substr(1,2),16);
    var gf = parseInt(from.substr(3,2),16);
    var bf = parseInt(from.substr(5,2),16);
    var rt = parseInt(to.substr(1,2),16);
    var gt = parseInt(to.substr(3,2),16);
    var bt = parseInt(to.substr(5,2),16);
    
    var r,g,b,h;
    var fa = [];
    while (frame < frames)
    {
      r = Math.floor(rf * ((frames-frame)/frames) + rt * (frame/frames));
      g = Math.floor(gf * ((frames-frame)/frames) + gt * (frame/frames));
      b = Math.floor(bf * ((frames-frame)/frames) + bt * (frame/frames));
      h = this.make_hex(r,g,b);
      
      fa.push(h);
      
      frame++;
    }
    fa.push(to);
    setTimeout(function() { Fat.update_fade(elem, delay, fa); }, delay);
  },
  
  update_fade : function (elem, delay, fa)
  {
    Fat.set_bgcolor(elem, fa.shift());
    if (fa.length)
      setTimeout(function() { Fat.update_fade(elem, delay, fa); }, delay);
  },
  
  set_bgcolor : function (o, c)
  {
    o.style.backgroundColor = c;
  },
  
  get_bgcolor : function (o)
  {
    while (o) {
      var c;
      if (unsafeWindow.getComputedStyle) c = unsafeWindow.getComputedStyle(o,null).getPropertyValue("background-color");
      if (o.currentStyle) c = o.currentStyle.backgroundColor;
      if ((c != "" && c != "transparent") || o.tagName == "BODY") { break; }
      o = o.parentNode;
    }
    if (c == undefined || c == "" || c == "transparent") c = "#FFFFFF";
    var rgb = c.match(/rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/);
    if (rgb) c = this.make_hex(parseInt(rgb[1]),parseInt(rgb[2]),parseInt(rgb[3]));
    return c;
  }
};

replyALL.Fat = Fat;
replyALL.NoticeHandler = NoticeHandler;
replyALL.init();

