/**
*An unofficial, modified version of the work of:
*remy sharp/http://remysharp.com
*Twitter/@rem
*http://remysharp.com/2007/05/18/add-twitter-to-your-blog-step-by-step/
 *
*@params
*  cssIdOfContainer: e.g. twitters
*  options: 
*    {
*      id: {String|Array} username or array of usernames to get tweets for
*      count: {Int} 1-20, defaults to 1-max limit 20
*      prefix: {String} '%name% said', defaults to blank
*      clearContents: {Boolean} true, removes contents of element specified in cssIdOfContainer, defaults to true
*      ignoreReplies: {Boolean}, skips over tweets starting with '@', defaults to false
*      template: {String} HTML template to use for LI element (see URL above for examples), defaults to predefined template
*      enableLinks: {Boolean} linkifies text, defaults to true,
*      newwindow: {Boolean} opens links in new window, defaults to false
*      timeout: {Int} How long before triggering onTimeout, defaults to 10 seconds
*      onTimeoutCancel: {Boolean} Completely cancel twitter call if timedout, defaults to false
*      onTimeout: {Function} Function to run when the timeout occurs. Function is bound to element specified with 
*        cssIdOfContainer (i.e. 'this' keyword)
*      callback: {Function} Callback function once the render is complete, doesn't fire on timeout 
*      relativeTime : {Boolean|Integer} Whether to use relative time stamps or number of hours before absolute time is used, defaults to 48
*      linkTime : {Boolean} Whether the time should link to the user's twitter page, default false
*      maskLinkText: {String} replaces text of all links if present, default blank
*      maxLinkLength : {Int} maximum length of links, defaults to 25
*      maskLinks : {Array} Array of arrays containing search and replacement values to mask links with (see below).
 *
*   INTERNATIONALIZATION OPTIONS
*      monthNames : {Array} Array of month names, default abbreviated English names
*      amPm : {Array} Array of values for AM and PM, default ['AM', 'PM']
*      ordinalSuffix : {function (Int) } Function to return an ordinal suffix given an integer, ex: 'th', 'st', 'nd', 'rd'
 *
*   CURRENTLY DISABLED DUE TO CHANGE IN TWITTER API:
*      withFriends: {Boolean} includes friend's status
 *
*    }
 *
 <code>
    // maskLinks option example
    
    maskLinks : [
      
      // Simple replacement. Links containing 'bit.ly' will read 'more...' 
      // Entire text of link is replaced.
      
      ['bit.ly', 'more...'],
      
      // Regex replacement. Links containing 'facebook.com' will read 'facebook/{username}'
      // Only text matching regex is replaced. Second array member can be a function.
      
      [/^.*facebook\.com\/(?:people\/)?([^\/]+).*$/i , 'facebook/$1']
    ]
</code>
 *
*@license MIT (MIT-LICENSE.txt)
*@version 1.13.custom-unofficial new features and bug fixes
 */

// to protect variables from resetting if included more than once
if (typeof window.getTwitters!='function') (function () {
  
  // default options
  var defaultOptions={
    
    // control options
    id : '',
    count : 1,
    ignoreReplies : false,
    timeout : 10,
    onTimeoutCancel : false,
    onTimeout : null,
    callback : null,
    
    // display options
    clearContents : true,
    enableLinks : true,
    relativeTime : 48,
    linkTime : false,
    maskLinkText : '',
    maxLinkLength : 25,
    maskLinks : [],
    prefix : '',
    template : '<span class="twitterStatus">%text%</span><span class="twitterTime"> %time% </span>',
    
    // language options
    monthNames : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    amPm : ['AM', 'PM'],
    ordinalSuffix : function(n) {return ['th','st','nd','rd'][n<4 || (n>20 && n % 10<4) ? n % 10 : 0]},
    
    // default filters (sorry, ify.)
    // based on Dustin Diaz's ify, but with my fixes :-)
    filters : {   
      link : function (t, options) {
        var lt=options.maskLinkText, ml=options.maskLinks, ll=options.maxLinkLength;
        return t.replace(/[a-z]+(:\/\/)?(www\.)?[a-z0-9-_]+\.[a-z0-9-_:~%&\?\/.=]{2,}[^!:\.,\(\)\s*$](\/)?/ig, 
          function (m, m1, m2, m3) {
            if (!m1 && !ml && !lt && m.indexOf('/')<4 && m.length-m.lastIndexOf('.')>4) return m;
            if (ml && ml.length) for (var i=ml.length; i--;) {
              if (typeof ml[i][0]=='string' && typeof ml[i][1]!='function') {
                if (m.indexOf(ml[i][0])!=-1) return makeLink(ml[i][1]);
              } else {
                if (m.match(ml[i][0])) return makeLink(m.replace(ml[i][0], ml[i][1]));
              }
            }
            var sp = m2 ? m.indexOf(m2)+m2.length : m1 ? m.indexOf(m1)+m1.length : 0;
            return makeLink(lt || m.substr(sp, ll)+(m.length-sp>ll ? '...' : ''));
            function makeLink(text) { return ' <a href="'+(m1 ? '' : 'http://')+m+'">'+text+'</a> '}
          }
        );
      },
      
      at :    [ /(^|[^\w]+)\@([a-zA-Z0-9_]{1,15}(\/[a-zA-Z0-9-_]+)*)/g ,
                '$1@<a href="http://twitter.com/$2">$2</a>' ],
      
      hash :  [ /(^|[^&\w''""]+)\#([a-zA-Z0-9_]{2,})/g , 
                '$1<a href="http://search.twitter.com/search?q=%23$2">#$2</a>' ]
    }
  
  };
  
  // private statics
  var readyList=[], callbacks=[], timeouts=[], isReady=false;
  
  // getTwitters is public, so attach it to window object
  window.getTwitters=getTwitters;
  
  // allows default options to be modified
  getTwitters.getDefaults=getDefaults;
  getTwitters.setDefaults=setDefaults;
  
  // attach callbacks to the public function so the JSONP scripts can get to them
  getTwitters.callbacks=callbacks;
  
  // GO!
  DOMReady(); 
  
  // public functions
  
  function getTwitters (target, id, count, userOptions) {
    createCallbacks(getOptions(target, id, count, userOptions));
  }

  function getDefaults () { return defaultOptions; }
  
  function setDefaults (userOptions) {
    for (var i in userOptions) defaultOptions[i]=userOptions[i];
  }
  
  // private functions
  
  // apply filters
  function filter(text, options) {
    var fs=options.filters;
    for (f in fs) {
      if (!fs[f]) continue;
      if (typeof fs[f]=='object' && fs[f].length==2)
        text=text.replace(fs[f][0], fs[f][1]);
      else if (typeof fs[f]=='function') 
        text=fs[f](text, options);
    }
    return text;
  }
  
  // merge two objects into a third object
  function merge(a, b) {
    var c={}, p;
    if (a) for (p in a) c[p]=a[p];
    if (b) for (p in b) c[p]=b[p];
    return c;
  }
  
  // called by getTwitters; build an options object
  function getOptions (target, id, count, userOptions) {
    var options={}, cb={}, to={};
    
    if (typeof target=='object') {
      userOptions=target;
      id=userOptions.id;
      target=userOptions.target;
    } else if (typeof id=='object') {
      userOptions=id;
      id=userOptions.id;
    } else {
      if (!userOptions) userOptions={};
      userOptions.count=userOptions.count || count || 1;
    }
    
    userOptions.filters = merge(defaultOptions.filters, userOptions.filters);
    options=merge(defaultOptions, userOptions);

    // need to make these global since we can't pass in to the twitter callback
    options.target=target;  // DOM element
    options.id=id;          // user id(s)
    
    // private options
    options._cbid=callbacks.length; // we'll need to write this in the injected script URL
    options._cb=cb;          // object containing callback(s) for user(s)
    options._to=to;          // timeout reference
    
    callbacks.push(cb);
    
    // Hack to disable withFriends, twitter changed their API so this requires auth
    // http://getsatisfaction.com/twitter/topics/friends_timeline_api_call_suddenly_requires_auth
    if (options.withFriends) options.withFriends=false;
    
    return options;
  }
  
  // called by getTwitters; initialize JSONP script injection
  function createCallbacks(options) {
    var id=options.id, cb=options._cb;
    if (typeof id!='string') {
      for (var u=id.length; u--;) {
        cb[id[u]]=(function(thisId) { return function (obj) {
          var c, i, r, o=[];
          cb[thisId].result=obj;
          for (c=id.length; c--;) if (!cb[id[c]].result) return;
          for (c=id.length; c--;) { r=cb[id[c]].result; for (i=r.length; i--;) o.push(r[i]) }
          o.sort(function(a,b) {return parseDate(a.created_at)<parseDate(b.created_at) ? 1 : -1 });
          if (typeof options.onTimeout=='function') clearTimeout(options._to);
          renderTwitters(o, options);
          cb=null;
        }})(id[u]);
      }
      ready(function() { for (var i=id.length; i--;) injectScript(options, id[i]) });
    } else {
      cb[id]=function (obj) {
        if (typeof options.onTimeout=='function') clearTimeout(options._to);
        renderTwitters(obj, options);
      };
      ready(function() { injectScript(options, id) }); 
    }
  }
  
  // called by createCallbacks via ready(); inject JSONP scripts
  function injectScript(options, id) {
    // if the element isn't on the DOM, don't bother
    if (!document.getElementById(options.target)) return;
    var count=options.count ? options.ignoreReplies ? options.count*4 : options.count : 1;
    var url='http://www.twitter.com/statuses/' 
     +(options.withFriends ? 'friends_timeline' : 'user_timeline') 
     +'/'+id+'.json?callback=getTwitters.callbacks['+options._cbid+'].'
     +id+'&count='+count+'&nocache='+Math.random();
    if (typeof options.onTimeout=='function') {
      if (options._to) clearTimeout(options._to);
      options._to=setTimeout(function() {
        // cancel callback
        if (options.onTimeoutCancel) options._cb={};
        options.onTimeout.call(document.getElementById(options.target));
      }, options.timeout*1000);
    }
    var script=document.createElement('script');
    script.setAttribute('src', url);
    document.getElementsByTagName('head')[0].appendChild(script);
  }
  
  // called by a callback; put the tweets on the page
  function renderTwitters(obj, options) {

    var element=document.getElementById(options.target);
    var data, li, statusSpan, timeSpan, markup, ul=document.createElement('ul'); 
    var max=obj.length>options.count ? options.count : obj.length;
    
    for (var i=0; i<max && obj[i]; i++) {
      if (options.ignoreReplies && obj[i].text.substr(0, 1)=='@') { max++; continue}
      
      data=getTwitterData(obj[i]);
      data.time=fuzzyTime(data.created_at, options);
      markup='';
      
      if (options.prefix) {
        var p=options.prefix.replace(/%(.*?)%/g, function (m, l) { 
          return typeof obj[i].user[l]!='undefined' ? obj[i].user[l] : 
            data[l]!='undefined' ? data[l] : '';
        });
        markup += '<span class="twitterPrefix">'+p+'</span>';
      }
      
      markup += options.template.replace(/%([a-z_\-\.]*)%/ig, function (m, l) {
        var r=''+data[l] || '';
        if (l=='text' && options.enableLinks) return filter(r, options);
        if (l=='time' && options.linkTime) return '<a href="http://twitter.com/'
          +data.user_screen_name+'/statuses/'+data.id+'/">'+r+'</a>';
        return r;
      });
      
      if (options.newwindow) markup=markup.replace(/<a href/gi, '<a target="_blank" href');
      
      li=document.createElement('li');
      li.innerHTML=markup;
      ul.appendChild(li);
    }

    if (options.clearContents) while (element.firstChild) element.removeChild(element.firstChild);
    element.appendChild(ul);
    if (typeof options.callback=='function') options.callback.call(element);
  }
  
  // called by renderTwitters; don't inject the scripts until the DOM loads
  function getTwitterData(orig) {
    var data={};
    for (var i in orig) {
      var isHash=false;
      if (typeof orig[i]=='object') for (var j in orig[i]) { 
        data[i+'_'+j]=orig[i][j];
        isHash=true;
      }
      if (!isHash) data[i]=orig[i];
    }
    return data;
  }
  
  // called by createCallbacks; don't inject the scripts until the DOM loads
  function ready(callback) {
    if (!isReady) readyList.push(callback);
    else callback.call();
  }
  
  // called by DOMReady; lets the script know it's ok to do its thing
  function fireReady() {
    isReady=true;
    var fn;
    while (fn=readyList.shift()) fn.call();
  }

  // ready and browser adapted from John Resig's jQuery library (http://jquery.com)
  function DOMReady() {
    
    // only used for the DOM ready, since IE & Safari require special conditions
    var b=navigator.userAgent.toLowerCase(), browser={
      webkit:   /webkit|khtml/.test(b),
      msie:     /msie/.test(b) && !/opera/.test(b)
    };
 
    if (document.addEventListener && !browser.webkit) {
      document.addEventListener("DOMContentLoaded", fireReady, false);
    }
    else if (browser.msie) {
      // If IE is used, use the excellent hack by Matthias Miller
      // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited

      // Only works if you document.write() it
      document.write("<scr"+"ipt id=__ie_init defer=true src=//:><\/script>");

      // Use the defer script hack
      var script=document.getElementById("__ie_init");

      // script does not exist if jQuery is loaded dynamically
      if (script) script.onreadystatechange=function () {
        if (this.readyState!="complete") return;
        this.parentNode.removeChild(this);
        fireReady.call();
      }

      // Clear from memory
      script=null;
    }
    else if (browser.webkit) {
      // Continually check to see if the document.readyState is valid
      var safariTimer=setInterval(function () {
        // loaded and complete are both valid states
        if (/^(loaded|complete)$/.test(document.readyState)) {
          // If either one are found, remove the timer
          clearInterval(safariTimer);
          safariTimer=null;
          // and execute any waiting functions
          fireReady.call();
        }
      }, 10);
    }
  }
  
  // date / time stuff
  
  function formatTime(date, options) {
    var h=date.getHours(), m=''+date.getMinutes(), am=options.amPm;
    return "";//(h>12 ? h-12 : h)+':'+(m.length==1 ? '0' : '' )+m+' '+(h<12 ? am[0] : am[1]);
  }
  
  function formatDate(date, options) {
    var mon=options.monthNames[date.getMonth()],
        day=date.getDate(),
        year=date.getFullYear(),
        thisyear=(new Date()).getFullYear(),
        suf=options.ordinalSuffix(day);

    return mon+' '+day+suf+(thisyear!=year ? ', '+year : '');
  }
  
  function parseDate(str) {
    var v=str.split(' ');
    return new Date(Date.parse(v[1]+" "+v[2]+", "+v[5]+" "+v[3]+" UTC"));
  }
    
  function fuzzyTime(timeValue, options) {
    var date=parseDate(timeValue),
        delta=parseInt(((new Date()).getTime()-date.getTime())/1000),
        relative=options.relativeTime,
        cutoff=+relative===relative ? relative*60*60 : Infinity;
    
    if (relative===false || delta>cutoff)
      return formatTime(date, options)+' '+formatDate(date, options);
    
    if (delta<60) return 'less than a minute ago';
    var minutes=parseInt(delta/60 +0.5);
    if (minutes <= 1) return 'about a minute ago';
    var hours=parseInt(minutes/60 +0.5);
    if (hours<1) return minutes+' minutes ago';
    if (hours==1) return 'about an hour ago';
    var days=parseInt(hours/24 +0.5);
    if (days<1) return hours+' hours ago';
    if (days==1) return formatTime(date, options)+' yesterday';
    var weeks=parseInt(days/7 +0.5);
    if (weeks<2) return formatTime(date, options)+' '+days+' days ago';
    var months=parseInt(weeks/4.34812141 +0.5);
    if (months<2) return weeks+' weeks ago';
    var years=parseInt(months/12 +0.5);
    if (years<2) return months+' months ago';
    return years+' years ago';
  }
})();


