// ==UserScript==
// @name          feedUS
// @namespace     net.moeffju.dA
// @description	  deviantFEEDER User Script component
// @include       http://*.deviantart.com/*
// ==/UserScript==

/* feedUS © 2006 Matthias Bauer. All rights reserved. */

/*

Version 0.2
===========

0.2
 - actually limit the number of items shown in the MC =p
 - only update when in the MC for maximum freshness
0.15
 - markAllAsRead works now
0.1
 - First release

*/

/*
 * feedUS is © 2006 Matthias Bauer <http://moeffju.net/>
 * All rights reserved.
 */
var feedUS = {
  // const
  SCRIPT_NAME : 'feedUS',
  SCRIPT_URL : 'http://moeffju.net/dA/hack/js/feedUS/',
  
  UPDATE_URL : 'http://moeffju.net/dA/hack/js/update',
  VERSION : '0.2',
  
  REFRESH_INTERVAL : 30 * 60, // not less than 30 minutes
  
  FEED_HEADER_ICON : 'data:image/gif;base64,R0lGODlhDgAOAIQQALI8ANVJAeBoINxyKeyAJOiIJvOKG%2BOKTeaRQvucOPKjUuqqbeisgfXOpPTcw%2F39%2Bf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FyH5BAEKABAALAAAAAAOAA4AAAVxIAQAQWmagCgIDHMMRFwkCSkcT%2F40CE2XtxbDkWv4SoPBAVEoLIjGBBL3cCwSCmLCgEQ0GrlFQbEjlAjNMVFRAJsDMYOhgNgVGI%2FDGdyYgxELeSUFOnN4B4EMg3wGBAgvBwcCAQBycjEEK5opJCeeKSEAOw%3D%3D', // RSS icon
  
  // vars
  feedURL : null,
  feedEntries : {},
  lastRefreshed : null,
  seenItems : {},
  puttingFeedData : false,
  
  // refs
  NoticeHandler : 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);
  },
  
  // ctime
  now : function() {
    return Math.floor(new Date().getTime() / 1000);
  },
  
  // 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', -1);
    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);
        }
      },
      data: null
    });
  },
  
  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.002);
  },
  
  // init
  init : function () {
    this.checkVersion();
    this.main();
  },
  
  // main
  main : function () {
    this.feedURL = GM_getValue('feedURL');
    this.lastRefreshed = GM_getValue('lastRefreshed', 0);
    
    if (!this.feedURL) {
      // XXX notify user how to setup feed(s)
      // test feed
      this.feedURL = 'http://moeffju.net/dA/news/feed-atom.php?categories%5B%5D=announcements&categories%5B%5D=general&categories%5B%5D=front&categories%5B%5D=hottopics';
      GM_setValue('feedURL', this.feedURL);
    }
    
    var now = this.now();
    var x;
    
    // I wish GM had a JSON storage module =p
    eval('x = ' + GM_getValue('feedEntries', '{}'));
    this.feedEntries = x;
    this.sortEntries();
    eval('x = ' + GM_getValue('seenItems', '{}'));
    this.seenItems = x;
    
    if (this.isMC()) {
      this.addFeedHolder();
      this.putFeedData();
      
      if (this.lastRefreshed < now - this.REFRESH_INTERVAL) {
        GM_log('refreshing ('+this.lastRefreshed+'/'+now+') every '+this.REFRESH_INTERVAL);
        this.refreshFeed();
      }
      else {
        GM_log('not refreshing ('+this.lastRefreshed+'/'+now+')');
      }
    
    }
    
    if (this.hasDeviantBar()) {
      this.addMessageCountHolder();
      this.updateMessageCount();
    }
  },
  
  isMC : function() {
    //return (/^(http:\/\/my.deviantart.com\/messages\/)$/.test(window.location.href));
    return ('http://my.deviantart.com/messages/' == document.location.href);
  },
  
  hasDeviantBar : function() {
    return true;
  },
  
  refreshFeed : function() {
    var self = this;
    
    GM_xmlhttpRequest({
      method: 'GET',
      url: this.feedURL,
      headers: {
        'User-Agent': [navigator.userAgent, ' Greasemonkey (', this.SCRIPT_NAME, ')'].join(''),
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      onload: function(response) {
        if (response.status == 200) {
          self.updateFeed(response.responseText.replace(/<\?xml.*?\?>/g, ""));
        }
        else if (response.status == 304) {
          // No updates
        }
        else {
          self.NoticeHandler.displayNotice(
            '<b>Error updating feed:</b> Error Code ' + response.status + " " + response.statusText,
            '#ff8080', '#ff0000', 20, 0.001);
        }
      },
      data: null
    });
  },
  
  parseAtomDate : function(atomDate) {
    if (atomDate && atomDate.length != 25) return new Date(0);
    
    var Y = atomDate.substr(0, 4);
    var m = atomDate.substr(5, 2);
    var d = atomDate.substr(8, 2);
    var H = parseInt(atomDate.substr(11, 2));
    var M = parseInt(atomDate.substr(14, 2));
    var S = parseInt(atomDate.substr(17, 2));
    var th = parseInt(atomDate.substr(19, 3));
    var tm = parseInt(atomDate.substr(23, 2));
    
    // this might be wrong
    if (th < 0) tm *= -1;
    M += tm; while (M < 0) { M += 60; H--; }
    H += th; while (H < 0) H += 24;
    
    return new Date(Y,m-1,d,H,M,S);
  },
  
  formatDate : function(dt) {
    var mn = 'JanFebMarAprMayJunJulAugSepOctNovDec';
    return mn.substr(dt.getMonth()*3, 3) + ' ' + dt.getDate() + ', ' + dt.getFullYear() + ', ' + [dt.getHours() % 12, (dt.getMinutes() < 10 ? '0'+dt.getMinutes() : dt.getMinutes())].join(':') + (dt.getHours() < 12 ? ' AM' : ' PM');
  },
  
  updateFeed : function(feedData) {
    this.lastRefreshed = this.now();
    GM_setValue('lastRefreshed', this.lastRefreshed);
    
    var feed;
    try {
      feed = new XML(feedData);
    }
    catch (e) {
      this.NoticeHandler.displayNotice('<b>Error parsing feed:</b> ' + e, '#ff8080', '#ff0000', 10, 0.001);
      return;
    }
    
    var atom = new Namespace('http://www.w3.org/2005/Atom');
    var entries = this.feedEntries;
    
    GM_setValue('feedUID', feed.atom::id.toString());
    GM_setValue('feedUpdated', feed.atom::updated.toString());
    
    for each (var entry in feed.atom::entry) { // I heart E4X
      entries[entry.atom::id.toString()] = {
        id : entry.atom::id.toString(),
        title : entry.atom::title.toString(),
        updated : entry.atom::updated.toString(),
        link : entry.atom::link.@href.toString(),
        summary : entry.atom::summary.toString(),
        updated_js : this.parseAtomDate(entry.atom::updated.toString()),
        catid : entry.atom::category.@term.toString(),
        catname : entry.atom::category.@label.toString()
      };
    }
    
    for (var entryId in this.seenItems) {
      delete(entries[entryId]); // these have been removed from the mc already, so don't re-add them
    }
    
    this.feedEntries = entries;
    this.sortEntries();
    
    GM_setValue('feedEntries', uneval(this.feedEntries));
    
    if (this.isMC()) this.putFeedData();
  },
  
  sortEntries : function() {
    var entries = this.feedEntries;
    var ts = [];
    for (var entryId in entries) { ts.push(entryId); }
    ts.sort(function(a,b) { return entries[a]['updated_js'].getTime() > entries[b]['updated_js'].getTime() ? -1 : 1; });
    entries = {};
    for (var i = 0; i < ts.length; i++) { entries[ts[i]] = this.feedEntries[ts[i]]; }
    this.feedEntries = entries;
  },
  
  markAllAsRead : function(e) {
    for (var entryId in this.feedEntries) { this.seenItems[entryId] = 1; delete(this.feedEntries[entryId]); }
    
    GM_setValue('seenItems', uneval(this.seenItems));
    GM_setValue('feedEntries', uneval(this.feedEntries));

    this.putFeedData();
  },
  
  markAsRead : function(e, id) {
    this.seenItems[id] = 1;
    delete(this.feedEntries[id]);
    
    GM_setValue('seenItems', uneval(this.seenItems));
    GM_setValue('feedEntries', uneval(this.feedEntries));
    
    this.putFeedData();
  },
  
  addFeedHolder : function() {
    var self = this;
    
    var div = document.createElement('div');
    div.id = 'feed-holder';
    div.className = 'container subsection';
    div.style.display = 'none';
    
    var h = document.createElement('h3');
    h.id = 'subsection-head-feed';
    
    var h_img = document.createElement('img');
    h_img.className = 'icon';
    h_img.src = this.FEED_HEADER_ICON;
    h_img.style.margin = '2px';
    
    var h_text = document.createElement('span');
    h_text.id = 'feed-header-text';
    h_text.textContent = 'No data';
    
    var h_aside = document.createElement('div');
    h_aside.id = 'feed-header-aside';
    h_aside.className = 'aside-head';
    
    h.appendChild(h_img);
    h.appendChild(document.createTextNode(' '));
    h.appendChild(h_text);
    
    div.appendChild(h_aside);
    div.appendChild(h);
    
    var notice_text = document.createElement('p');
    notice_text.id = 'feed-notice-text';
    notice_text.style.margin = '12px 0 12px 13px';
    notice_text.textContent = 'Feed has not been updated yet.';
    
    div.appendChild(notice_text);
    
    var div_foot = document.createElement('div');
    div_foot.id = 'feed-footer';
    div_foot.className = 'nav-holder float-holder section-foot';
    
    var div_foot_text = document.createElement('p');
    div_foot_text.id = 'feed-footer-text';
    div_foot_text.className = 'aside-left';
    div_foot_text.textContent = 'No data';
    
    var div_foot_bt = document.createElement('input');
    div_foot_bt.id = 'feed-footer-btremove';
    div_foot_bt.type = 'button';
    div_foot_bt.className = 'button';
    div_foot_bt.value = 'Remove All';
    div_foot_bt.addEventListener('click', function(e) { self.markAllAsRead(); }, true);
    div_foot_bt.disabled = true;
    
    div_foot.appendChild(div_foot_text);
    div_foot.appendChild(div_foot_bt);
    
    div.appendChild(div_foot);
    
    var parent = document.getElementById('message-center');
    var target = parent.getElementsByTagName('div')[0];
    
    parent.insertBefore(div, target);
  },
  
  putFeedData : function() {
    var self = this;
    
    if (this.puttingFeedData) { // already updating
      setTimeout(self.putFeedData, 500); // call back in half a second
      return; // hang up
    }
    
    // put new data into holders
    if (!document.getElementById('feed-holder')) {
      GM_log('Something HORRIBLE happened! HOLD ME!');
      return;
    }
    
    this.puttingFeedData = true;
    
    var entries = this.feedEntries;

    var div = document.getElementById('feed-holder');
    var h_text = document.getElementById('feed-header-text');
    var h_aside = document.getElementById('feed-header-aside');
    var div_foot = document.getElementById('feed-footer');
    var div_foot_text = document.getElementById('feed-footer-text');
    var div_foot_bt = document.getElementById('feed-footer-btremove');
    var notice_text = document.getElementById('feed-notice-text');
    
    div.style.display = 'block';
    
    if (this.lastRefreshed == 0) {
      h_aside.textContent = 'Feed not loaded';
    }
    else {
      h_aside.textContent = 'refreshed ' + this.formatDate(new Date(this.lastRefreshed * 1000));
    }
    
    var limit = 15;
    
    var n = 0;
    for (var id in entries) { n++; }
    
    // remove old ULs
    var uls = div.getElementsByTagName('ul');
    for (var i = uls.length - 1; i >= 0; i--) { uls[i].parentNode.removeChild(uls[i]); }

    if (n == 0) {
      div_foot_bt.disabled = true;
      h_text.textContent = 'No feed items';
      div_foot_text.textContent = 'Nothing to show';
      notice_text.textContent = "No items found. Either the feed is empty or erroneous.";
      notice_text.style.display = 'block';
    }
    else {
      div_foot_bt.disabled = false;
      h_text.textContent = n + ' feed items';
      div_foot_text.textContent = 'Showing '+Math.min(n,limit)+' of '+n+' feed items';
      notice_text.style.display = 'none';
      
      var i = 1;
      for (var id in entries) {
        if (i == limit) break; // limit
        var ul;
        var li;
        var ul_id = 'feed-item-ul-'+entries[id]['updated_js'].getTime();
        // XXX handle case when we put fewer entries than exist!
        if (ul = document.getElementById(ul_id)) {
          li = ul.getElementsByTagName('li')[0];
          li.innerHTML = ''; // hacky but simple :p
        }
        else {
          ul = document.createElement('ul');
          li = document.createElement('li');
        }
        
        ul.className = 'beacon';
        
        li.className = (i % 2 == 0 ? 'even' : 'odd');
        if (i == 1) li.className = 'first '+li.className;
        else if (i == n) li.className = 'last '+li.className;
        
        var span_main = document.createElement('span');
        span_main.className = 'main';
        
        var img_x = document.createElement('img');
        img_x.src = 'http://i.deviantart.com/icons/misc/x.gif';
        //img_x.className = 'remove';
        img_x.width = '18';
        img_x.height = '18';
        img_x.alt = '[X]';
        img_x.title = 'Remove';
        
        img_x.addEventListener('click', function(lid){ return function(e) { self.markAsRead(e, lid); } }(id), true);
        
        //var img_news = document.createElement('img');
        
        var a_link = document.createElement('a');
        a_link.href = entries[id]['link'];
        a_link.textContent = entries[id]['title'];
        
        span_main.appendChild(img_x);
        span_main.appendChild(document.createTextNode(' '));
        span_main.appendChild(a_link);
        
        if (entries[id]['catid']) { // TODO multiple categories in feed
          var a_cat = document.createElement('a');
          a_cat.href = 'http://news.deviantart.com/browse/' + entries[id]['catid'] + '/';
          a_cat.textContent = entries[id]['catname'];
          a_cat.title = 'Browse news entries in '+entries[id]['catname'];
  
          span_main.appendChild(document.createTextNode(' in '));
          span_main.appendChild(a_cat);
        }
        
        var span_ext = document.createElement('span');
        span_ext.className = 'ext';
        span_ext.textContent = this.formatDate(entries[id]['updated_js']);
        
        li.appendChild(span_main);
        li.appendChild(span_ext);
        ul.appendChild(li);
        
        div.insertBefore(ul, notice_text);
        
        i++;
      }
    }
    
    this.puttingFeedData = false;
  },
  
  addMessageCountHolder : function() {
    // XXX TODO
  },
  
  updateMessageCount : function() {
    /// XXX TODO
  },
};

/*
 * 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;
  },
  
};

feedUS.NoticeHandler = NoticeHandler;
feedUS.init();

