/**********************************************************************************
 * The below code is an altered portion of code from newsyndicate. This code is   *
 *  copywritten by Joshua Urbain and is being used with the author's permission.  *
 **********************************************************************************/

/**
 * Set the width of the reader box using Javascript to speed up the expression in the CSS
 **/
function rssReadImgWidthSet(elem) {
   elem.width = (-1 != navigator.userAgent.indexOf("MSIE")) && elem.width > 235 ? "235px" : "auto";
}

/**
 * News Feed Reader object reads data drawn from the AJAX requests from the specific URL
 **/
var NewsFeed = Class.create();
NewsFeed.prototype = {

   /**
    * Constructor: Sets the properties of the class
    *
    * @param string viewId  The ID of the container object
    * @param string url     URL of the Feed to read
    * @param string objId   The object IDs to be named with this feed
    **/
   initialize: function(viewId, url, objId, primeData) {
      /**
       * The ID of the container object
       *
       * @var string
       **/
      this.id          = viewId;

      /**
       * The container object's reference
       *
       * @var object
       **/
      this.displaybox  = $(this.id);

      /**
       * URL of the feed
       *
       * @var string
       **/
      this.url         = url;

      /**
       * The ID for the objects in this feed
       *
       * @var string
       **/
      this.appendedid  = objId ? objId : 'rssreader_';

      /**
       * Are we using Internet Explorer?
       *
       * @var bool
       **/
      this.isIE        = window.navigator.userAgent.indexOf('MSIE') > 0;

      /**
       * Collection of article objects
       *
       * @var array
       **/
      this.articles    = [];

      /**
       * The channel cross-references between the elements to save and the actual tag names
       *
       * @var array
       **/
      this.channelXref = {
        'title'       : 'title',
        'description' : 'desc',
        'subtitle'    : 'desc',
        'link'        : 'url'
      };

      /**
       * The feed cross-references between the elements to save and the actual tag names
       *
       * @var array
       **/
      this.feedXref = {
        'title'       : 'title',
        'description' : 'desc',
        'summary'     : 'desc',
        'link'        : 'url',
        'content'     : 'content',
        'updated'     : 'updt',
        'pubDate'     : 'updt'
      };


      this._injectBehaviors();
      this._initAjax(primeData);
   },

   /**
    * Injects the behavior into the container tag using the data drawn from the caller
    *
    * @access private
    **/
   _injectBehaviors: function() {
      /**
       * @var object this.linkElem       Name of the feed to be displayed
       * @var object this.descElem       Description of the feed to be displayed
       * @var object this.waitcontainer  Waiting message/image container
       * @var object this.blockElem      Block where the articles are placed for the feed
       **/

      // If we already have a name object with the object's ID, use it for our operations
		if (document.getElementById(this.appendedid + 'name')) {
			this.linkElem   = document.getElementById(this.appendedid + 'name');
		} else {
	      var link        = document.createElement('a');
   		link.className  = 'rssreader_name';
	   	this.linkElem   = link;
		   this.displaybox.appendChild(link);
		}

      // If we already have a description object with the object's ID, use it for our operations
		if (document.getElementById(this.appendedid + 'desc')) {
         this.descElem   = document.getElementById(this.appendedid + 'desc');
		} else {
   		var descr       = document.createElement('div');
		   descr.className = 'rssreader_desc';
	   	this.descElem   = descr;
   		this.displaybox.appendChild(descr);
		}

      // Block element behavior
		this.blockElem     = document.createElement('div');
		this.blockElem.className = 'rssreader_block';
      // Create container for the waiting message/image
		this.waitcontainer = document.createElement('div');
      // Waiting image
		var wait           = document.createElement('img');
		wait.src           = 'ub/images/wait.gif';
		wait.style.margin  = '0 0 0 169px';
		wait.style.width   = '16px';
		wait.style.height  = '16px';
		// Waiting message
		var waitmsg        = document.createElement('span');
		waitmsg.innerHTML  = ' Loading...';

		// Fake a message in order to delete indicator.
		this.waitcontainer.className = this.id + 'message';
		// Add the image/message to the waiting container
		this.waitcontainer.appendChild(wait);
		this.waitcontainer.appendChild(waitmsg);
		// Add the waiting container to the block element
		this.blockElem.appendChild(this.waitcontainer);
		// Add the block element to the display container
		this.displaybox.appendChild(this.blockElem);
   },

   /**
    * Initializes AJAX calls to be periodic and gives an inital call to fill the object
    *
    * @access private
    **/
   _initAjax: function(primeData) {
      if (primeData) {
        // Slight hack here: we create an object that looks like the AJAX response with
        // an XML parser's results in it.
        var xmlDoc = new Object;
        //Internet Explorer
        if (this.isIE) {
           xmlDoc.responseXML = new ActiveXObject("Microsoft.XMLDOM");
           xmlDoc.responseXML.async = "false";
           xmlDoc.responseXML.loadXML(primeData);
        } else {
           //Firefox, Mozilla, Opera, etc.
           try {
              var parser = new DOMParser();
              xmlDoc.responseXML = parser.parseFromString(primeData, "text/xml");
           } catch(e) { xmlDoc.responseXML = null; }
        }
		this._ajaxResponse(xmlDoc);
	  } else {
		this._callAjax();
	  }

      new PeriodicalExecuter(this._callAjax.bindAsEventListener(this), 900);
   },

   /**
    * Calls the AJAX, setting up a success/failure spot to send data
    *
    * @access private
    **/
   _callAjax: function() {
      new Ajax.Request(this.url,
       {      method: 'get',
           onSuccess: this._ajaxResponse.bindAsEventListener(this),
           onFailure: this._errorHandler.bindAsEventListener(this),
        asynchronous: true});
   },

   /**
    * The listener for the AJAX when a successful call has been made, parsing the root nodes and sending to RSS or Atom parsing
    *
    * @access private
    * @param  object  t  Data from the AJAX call
    **/
   _ajaxResponse: function(t) {
      var xml = t.responseXML;
  	   if (xml) {
  	      // RSS tag means it's a RSS feed
         if (xml.getElementsByTagName('rss').length)
            this._processRSSfeed(xml.getElementsByTagName('rss')[0], false);
         // feed tag means it's an ATOM feed
         else if (xml.getElementsByTagName('feed').length)
            this._processATOMfeed(xml.getElementsByTagName('feed')[0]);
         // RDF-namespaced data is a RSS feed
         else if (this._getElementNS('rdf', 'RDF', xml, 0))
            this._processRSSfeed(this._getElementNS('rdf', 'RDF', xml, 0), true);
      }
   },

   /**
    * Processes the ATOM feed by channel and by each item
    *
    * @access private
    * @param  object xml  XML data from the main node of the ATOM feed
    **/
   _processATOMfeed: function(xml) {
      this._sendChannel(xml);
      // Send all the entries to the block container
      this._sendItems(xml.getElementsByTagName('entry'));
   },

   /**
    * Processes the RSS feed depending on the version
    *
    * @access private
    * @param  object xml    XML data from the main node of the RSS feed
    * @param  bool   rssone Is this an RSS 1.0 feed?
    **/
   _processRSSfeed: function(xml, rssone) {
      // If we are RSS 1.0, we can parse now
      if (rssone) {
         this._sendChannel(xml.getElementsByTagName('channel')[0]);
      // Otherwise, go to the channel tag and parse from there
      } else {
         xml = xml.getElementsByTagName('channel')[0];
         this._sendChannel(xml);
      }
      // Send all the items to the block container
      this._sendItems(xml.getElementsByTagName('item'));
   },

   /**
    * Sends the channel information to the appropriate areas
    *
    * @access private
    * @param  object xml  XML channel data to parse
    **/
   _sendChannel: function(xml) {
      // Default the URL of the link to '#' and title / description to a default
      var data = {
        'url'   : '#',
        'title' : 'Untitled Feed',
        'desc'  : ''
      };

      // Attempt to parse data, and then alternate sources if the first isn't available
      for(var i = 0; i < xml.childNodes.length; i++) {
         switch (xml.childNodes[i].nodeName) {
            case 'title':
            case 'description':
            case 'subtitle':
            case 'link':
               if (xml.childNodes[i].firstChild)
                  data[this.channelXref[xml.childNodes[i].nodeName]] = xml.childNodes[i].firstChild.nodeValue;
               else if (xml.childNodes[i].nodeName == 'link' && xml.childNodes[i].getAttribute('href'))
                  data[this.channelXref[xml.childNodes[i].nodeName]] = xml.childNodes[i].getAttribute('href');

               break;
         }
      }

      // Set the name of the link
      this.linkElem.innerHTML = data['title'];
      // Set the URL of the link
      this.linkElem.href      = data['url'];
      // Set the description text
      this.descElem.innerHTML = data['desc'];
   },

   /**
    * Sends the news items to the display container
    *
    * @access private
    * @param  object items  XML news items to parse
    **/
   _sendItems: function(items) {
      // If we have the wait container still, remove it and clear the reference
      if (this.waitcontainer) {
         Element.remove(this.waitcontainer);
         this.waitcontainer = null;
      }

      // Go from the highest item to the lowest, to push onto the stack effectively
      for (var i = 0; i < items.length; i++) {

         // Set the default values of the data
         var data = {
           'title'   : 'Untitled',
           'desc'    : '',
           'content' : '',
           'url'     : '#',
           'updt'    : 0
         };

         for(var j = 0; j < items[i].childNodes.length; j++) {
            switch (items[i].childNodes[j].nodeName) {
               case 'title':
               case 'description':
               case 'summary':
               case 'link':
               case 'content':
               case 'updated':
               case 'pubDate':
                  if (items[i].childNodes[j].firstChild) {
                     data[this.feedXref[items[i].childNodes[j].nodeName]] = items[i].childNodes[j].firstChild.nodeValue;

                     if (items[i].childNodes[j].nodeName == 'updated')
                        data['updt'] = this._convertATOMDate(data['updt']);
                     else if (items[i].childNodes[j].nodeName == 'pubDate')
                        data['updt'] = this._convertRSSDate(data['updt']);

                  } else if (items[i].childNodes[j].nodeName == 'link' && items[i].childNodes[j].getAttribute('href')) {
                     // Add something to check for href attribute
                     data[this.feedXref[items[i].childNodes[j].nodeName]] = items[i].childNodes[j].getAttribute('href');
                  }

                  break;
            }
         }

         // If we havn't got the content from the feed, try to see if it's in a namespace
         if (data['content'] == '')
            try { data['content'] = this._getElementTextNS('content', 'encoded', items[i], 0); } catch(e) {}

         // Check if the articles have this entry already
         if (this.articles[data['title'] + '_' + data['updt']]) {
            this.articles[data['title'] + '_' + data['updt']].Update(data['title'],
              data['updt'], data['desc'], data['content'], data['url']);
         } else {
            // If the feed isn't found, add the new entry
            this.articles[data['title'] + '_' + data['updt']] = new Article(this.blockElem,
              this.id, data['title'], data['updt'], data['desc'], data['content'], data['url']);
         }
      }

      // If we have Shadowbox, update it for the new content that might use it
      if (typeof(Shadowbox) != 'undefined')
         Shadowbox.setup();
   },

   /**
    * Converts the ATOM date/timestamp to logical format
    *
    * @access private
    * @param  string date  Date/timestamp for article
    * @return string       Logically-formatted date/timestamp
    **/
	_convertATOMDate: function(date) {
      // 2007-12-19T12:50:58Z
      // 01234567890123456789
      if (date.length == 20) {
         var month = date.substr(5, 2);

         // Trim the first 0 off for parseInt to work correctly
         if (month.length > 1 && month.substr(0, 1) == '0')
            month = month.substr(1, 1);

         // new Date(year, month, day, hours, minutes, seconds, milliseconds)
         return new Date(date.substr(0, 4), parseInt(month) - 1, date.substr(8, 2),
           date.substr(11, 2), date.substr(14, 2), date.substr(17, 2));
      }

		return -1;
	},

	/**
	 * Converts the RSS date/timestamp to logical format
	 *
	 * @access private
	 * @param  string date  Date/timestamp string data
	 * @return string       Logically-formatted date/timestamp
	 **/
	_convertRSSDate: function(date) {
      // Converts RFC 822 datestamp to readable data via regular expressions
      var ex = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), )?(\d{1,2}) ([a-zA-Z]{3}) (\d{2,4}) (\d{2}):(\d{2})(?:[:](\d{2}))? (UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|A|B|C|D|E|F|G|H|I|M|N|Y|Z|(?:[+-]\d{4}))\s*$/;
      // Execute the regular expression on the date string and place the results in m
      var m  = ex.exec(date);

      // If m is null, return nothing
      if (m == null)
	      return -1;

	   // Otherwise, if it's an array, display the logical format
      else {
	      // 1 = Day, 2 = Month, 3 = Year, 4 = Hour, 5 = Minutes, 6 = Seconds, 7 = Time adjust
   	   return new Date(m[3], this._translateMonth(m[2]), m[1], m[4], m[5], m[6]);
      }
   },

   /**
    * Converts a month's short name into it's value
    *
    * @access private
    * @param  string  monthShortName  Short name of the month (Jan = January)
    * @return integer                 Value of the number for the month (January = 0)
    **/
   _translateMonth: function(monthShortName) {
      var months = {'Jan' : 0, 'Feb' : 1, 'Mar' : 2, 'Apr' : 3,
        'May' :  4, 'Jun' : 5, 'Jul' : 6, 'Aug' : 7, 'Sep' : 8,
        'Oct' :  9, 'Nov' : 10, 'Dec' : 11};
      return months[monthShortName];
   },

   /**
    * Retrieve text of an XML document element, including elements using namespaces. This code is based upon examples from the Developer's Zone at Apple.com.
    *
    * @access private
    * @param  string  prefix      Namespace of the XML element
    * @param  string  local       Name of the XML element
    * @param  object  parentElem  The parent element where the element should be included
    * @param  integer index       Element with this name should we look at
    * @return object              The XML element returns
    **/
   _getElementNS: function(prefix, local, parentElem, index) {
      return (prefix && this.isIE) ?
		  parentElem.getElementsByTagName(prefix + ':' + local)[index] :
        parentElem.getElementsByTagName(local)[index];
   },

   /**
    * Grabs text from an XML node using the _getElementNS operation and processing it
    *
    * @access private
    * @param  string  prefix      Namespace of the XML element
    * @param  string  local       Name of the XML element
    * @param  object  parentElem  The parent element where the element should be included
    * @param  integer index       Element with this name should we look at
    * @return string              The text data from the XML element
    **/
   _getElementTextNS: function(prefix, local, parentElem, index) {
      var result = this._getElementNS(prefix, local, parentElem, index);

      // Get text, accounting for possible whitespace (carriage return) text nodes
      if (result)
         return (result.childNodes.length > 1) ? result.childNodes[1].nodeValue :
		     result.firstChild.nodeValue;
      return '';
   },

   /**
    * Error-handler if an AJAX call has failed
    *
    * @access private
    **/
   _errorHandler: function(t) {
      alert('An error occured while receiving news.');
   }

};


/**
 * Articles class holds entries of news for the user to receive via the feed class.
 **/
var Article = Class.create();
Article.prototype = {
   /**
    * Constructor: Sets the properties of the class
    *
    * @param object parent   Element to place the article
    * @param string id       ID to prefix to the article IDs
    * @param string title    Title of the article
    * @param string updt     Time of update for the article
    * @param string desc     Description of the article
    * @param string content  Content in the article
    * @param string url      URL of the article
    **/
   initialize: function(parent, id, title, updt, desc, content, url) {
      /**
       * The message container for the link, description, title, etc
       *
       * @var object
       **/
      this.divElem                    = document.createElement('div');

      // Behavior for the message
      this.divElem.className          = id + 'message';
      this.divElem.style.borderBottom = '1px solid #555';
      this.divElem.style.marginBottom = '3px';

      // Tie this class to the div element
      this.divElem.obj                = this;

      /**
       * Link to the article
       *
       * @var object
       **/
		this.aElem                      = document.createElement('a');

		// Behavior of the link
      this.aElem.innerHTML            = title;
      this.aElem.href                 = url;
      this.aElem.target               = '_blank';
      this.aElem.style.fontWeight     = 'bold';

      // Place the link in the container
      this.divElem.appendChild(this.aElem);

      // Add the time to the container
		this._AddTime(updt);
		// Add the description to the container
      this._AddDesc(desc);
      // Add the content to the container
      this._AddContent(content);

      // If this is the first message, just add it, otherwise make sure it is in order of the objects
      var newsObjs = parent.getElementsByTagName('div');
      var placed   = false;

      if (newsObjs.length) {
         for (var i = 0; !placed && i < newsObjs.length; i++) {
            // Insert the message right before the last item that is close, so we have an ordering
            if (newsObjs[i].className == id + 'message' && updt > newsObjs[i].obj.GetTime()) {
               parent.insertBefore(this.divElem, newsObjs[i]);
               placed = true;
            }
         }
      }

      if (!placed)
         parent.appendChild(this.divElem);
   },

   /**
    * Get the date/timestamp for the article
    *
    * @access public
    * @return string  The date/timestamp from the article data
    **/
	GetTime: function() {
		return this.time;
	},

	/**
	 * Sets the date/timestamp for the article
	 *
	 * @access public
	 * @return string  Date/timestamp to set article to
	 **/
	SetTime: function(time) {
		if (time != this.GetTime()) {
		   this.time               = time;
		   this.timeElem.innerHTML = this._translateMonth(time.getMonth()) + ' ' +
			  time.getDate() + ', ' + time.getFullYear() + ' ' +
			  this._convertTime(time.getHours(), time.getMinutes());
		}
	},

	/**
	 * Add the time to the article container if we have a time available
	 *
	 * @access private
	 * @param  string time  Time to display
	 **/
	_AddTime: function(time) {
		if (time) {
			this.timeElem = document.createElement('div');
			this.timeElem.className = 'rssreader_date';

         this.SetTime(time);

         this.divElem.appendChild(this.timeElem);
		}
	},

	/**
	 * Get the title for the article
	 *
	 * @access public
	 * @return string Title from the article data
	 **/
   GetTitle: function() {
      return this.aElem.innerHTML;
   },

   /**
    * Sets the title of the article
    *
    * @access public
    * @param  string title  The new title
    **/
   SetTitle: function(title) {
      if (title != this.GetTitle())
         this.aElem.innerHTML = title;
   },

   /**
    * Adds the descripion to the article container if we have a description available
    *
    * @access private
    * @param  string desc  Description to display
    **/
   _AddDesc: function(desc) {
      if (desc) {
         this.descElem           = document.createElement('div');
         this.descElem.innerHTML = this._formatText(desc);
         this.divElem.appendChild(this.descElem);
      }
   },

   /**
    * Gets the description (returns -1 if none available)
    *
    * @access public
    * @return string
    **/
   GetDesc: function() {
      if (this.descElem)
         return this.descElem.innerHTML;
      return -1;
   },

   /**
    * Sets the description
    *
    * @access public
    * @param  string desc  Sets the description only if it isn't already there and has the element created
    **/
   SetDesc: function(desc) {
      if (desc != this.GetDesc()) {
         if (this.descElem)
            this.descElem.innerHTML = this._formatText(desc);
         else
            this.AddDesc(desc);
      }
   },

   /**
    * Adds the content to the article container if we have content available
    *
    * @access private
    * @param  content desc  Content to display
    **/
   _AddContent: function(content) {
      if (content) {
         this.contentElem            = document.createElement('div');
         this.contentElem.innerHTML  = this._formatText(content);
         this.divElem.appendChild(this.contentElem);
      }
   },

   /**
    * Gets the content (returns -1 if none available)
    *
    * @access public
    * @return string
    **/
   GetContent: function() {
      if (this.contentElem)
         return this.contentElem.innerHTML;
      return -1;
   },

   /**
    * Sets the content
    *
    * @access public
    * @param  string content  Content to set to
    **/
   SetContent: function(content) {
      if (content != this.GetContent()) {
         if (this.contentElem) {
            this.contentElem.innerHTML = this._formatText(content);
         } else {
            this._AddContent(content);
         }
      }
   },

   /**
    * Gets the URL of the article
    *
    * @access public
    * @return string
    **/
   GetURL: function() {
      return this.aElem.href;
   },

   /**
    * Sets the URL of the article
    *
    * @access public
    * @param  string url  URL to set to
    **/
   SetURL: function(url) {
      if (url != this.GetURL())
         this.aElem.href = url;
   },

   /**
    * Updates the content of the article
    *
    * @access public
    * @param  string title    New title for the article
    * @param  string updt     New date/timestamp for the article
    * @param  string desc     New description for the article
    * @param  string content  New content for the article
    * @param  string url      New URL for the article
    **/
   Update: function(title, updt, desc, content, url) {
      if (title)     this.SetTitle(title);
		if (updt)      this.SetTime(updt);
      if (desc)      this.SetDesc(desc);
      if (content)   this.SetContent(content);
      if (url)       this.SetURL(url);
   },

   /**
    * @todo Figure out how to make _formatText, _convertTime, and _translateMonth static (or that equivalence)
    **/

   /**
    * Formats the text by clipping the XML blocks and stripping the string to 100 characters
    *
    * @access private
    * @param  string inputStr  String to process
    * @return string           Processed string
    **/
   _formatText: function(inputStr) {
      // Remove all commented areas (this happens a lot with Microsoft Word pastes)
      inputStr = inputStr.replace(/<!--.*-->/g, '');

//      // Change paragraph blocks into normal breaks
//      inputStr = inputStr.replace(/<(\/p)[^>]*>/ig, '<br /><br />');

      // Change the end of div blocks and headings to have breaks
      inputStr = inputStr.replace(/<(\/div|\/h[1-9])[^>]*>/ig, '<br />');

      // Only allow certain tags from being shown
//      inputStr = inputStr.replace(/<(?!a|href|img|br|\/a)(?:[^\>]*)>/ig, ' ');
      inputStr = inputStr.replace(/<(?!\/?(?:p|a|href|img|br))([^> ]+)[^>]*>/ig, '');

      // Reduce the content and if it keeps going, add an ellipse
      return (inputStr.length > 350 ? inputStr.substring(0, 350) + '&hellip;' : inputStr);
   },

	/**
	 * Converts the time from 24-hours to 12-hours with AM/PM
	 *
	 * @access private
	 * @param  string  hours   Hours to convert
	 * @param  string  minutes Minutes
	 * @return string          Converted time
	 **/
	_convertTime: function(hours, minutes) {
	   var ampm  = 'AM';

      // Trim the first 0 off for parseInt to work correctly
      if (hours.length > 1 && hours.substr(0, 1) == '0')
         hours = hours.substr(1, 1);

	   hours = parseInt(hours);
		if (hours > 11)
		   ampm   = 'PM';

		hours %= 12;
		if (hours == 0)
		   hours  = 12;

      return hours + ':' + (minutes < 10 ? '0' : '') + minutes + ' ' + ampm;
	},

   /**
    * Translates the month from a shortened name or a numerical version into the full name
    *
    * @access private
    * @param  string monthnum    The month number to convert to a full name
    * @return string             The full name month
    **/
	_translateMonth: function(monthnum) {
	   var months = ['January', 'February', 'March', 'April', 'May',
	     'June', 'July', 'August', 'September', 'October', 'November', 'December'];
	   return months[monthnum];
	}
};
