/* ****************************************************************************
Copyright (c) 2011 Kaldor Ltd 

Used to fetch tweets, and populate multiple elements on a
page with the results.

Can be pre-moderated by specifying a twitter list of allowed tweeters.

******************************************************************************/

// Set it up  
function TweetFetcher(options) {

  this.tweetList = this.emptyResults;
  this.apiCallsRemaining = 0;
  this.firstTweetList = true;
  this.firstAPICheck = true;
  
  this.attemptRESTCalls = false;

  this.nextToFill = 0;
  this.nextIndex = 0;
  this.firstCycle = true;

  this.settings = {
    query: null,
    accessToken: null,
    show: null,
    onLoad: null,
    onComplete: null,
    slots: null,
    moderating: false,          // Should we moderate?
    allowed_users: [],          // The initial list of allowed users. Overwritten if loaded from twitter list
    fetch_number: 50,         // How many do we fetch and cycle through
    fetch_freq: 1000 * 60,        // How often do we fetch tweets
    moderation_list: null,        // Twitter list used for moderation
    moderation_freq: 1000  * 5 * 60,  // How often do we update the moderation list
    api_limit_freq: 1000 * 10,      // How often do we check API calls remaining
    api_limit_cutoff: 50,       // When do we stop using our requests. TODO: Special instances via query string for extra?  
    tweet_cycle_freq: 8000 * 1,     // How often do we update the visible tweets
    offline_from_user: "NewsFlash",
    offline_text: "Go online to get live tweets",
    on_success: null,
    create_tweet_element: function(el, tweet) {
      var t = $('<div>')
      .append($('<b>').append(tweet.from_user + " "))
      .append(tweet.text);
      t.append(" : " + timeAgo(tweet.created_at));
      el.html(t);   
    },
    api_limit_exceeded: function() {
    },
    api_limit_okay: function() {
    },  
    search_limit_exceeded: function() {
    },
    search_limit_okay: function() {
    },  
    debug_element: null

  };
  
  options && $.extend(this.settings, options);

  this.settings.onLoad != null && typeof this.settings.onLoad == 'function' && this.settings.onLoad();         

  // Populate the initial moderation list
  this.rejectedTweeters = [];
  this.moderationMembers = this.settings.allowed_users;
  
  // Start checking the API limit
  this.getAPILimit();

  
};

/* ****************************************************************************
API Endpoints
******************************************************************************/
TweetFetcher.prototype.apiEndpoint = ('https:' == document.location.protocol ? 'https://' : 'http://') + "api.twitter.com/1";
TweetFetcher.prototype.searchEndpoint = ('https:' == document.location.protocol ? 'https://' : 'http://') + "search.twitter.com";

TweetFetcher.prototype.emptyResults = {"results": [] };

/* ****************************************************************************
Debug
*******************************************************************************/
TweetFetcher.prototype.debug = function(f, s) {
  if (this.settings.debug_element) {
    $(this.settings.debug_element).append("Tweeter: <b>"+f+"</b> " + s + "<br />");
  }
}

/* ****************************************************************************
Get the number of remaining calls to the REST API
******************************************************************************/
TweetFetcher.prototype.getMembersOfList = function(members, cursor) {


  var fetcher = this; 

  if (!this.attemptRESTCalls) {
    fetcher.debug("getMembersOfList", "Rate Limited");
    return;
  }

  if (!this.settings.moderation_list) {
    fetcher.debug("getMembersOfList", "Moderating with no list");
    return;
  }
  
  var requestUrl = this.apiEndpoint + "/" + this.settings.moderation_list + "/members.json?cursor=" + cursor;
  this.debug("getMembersOfList", requestUrl);

  if (members == null) members = [];
  if (cursor == null) cursor = -1;
  
  $.ajax({
    url: requestUrl,
    type: 'GET',
    dataType: 'jsonp',
    timeout: 10000,
    success: function(json){
    
      if (!json.users) alert("SHOULD NEVER HAPPEN"); 
      var user_array = json.users;
      for (var i=0; i<json.users.length; i++) {
        members.push(json.users[i].screen_name);
      }
      
      var next = json.next_cursor_str;
      
      if (next && next != 0) {
        fetcher.getMembersOfList(members, next);
      } else {
        // Managed to get through all the pages. Replace the list
        fetcher.debug("getMembersOfList", "Got new list of " + members.length);
        fetcher.moderationMembers = members;
      }
    },
    error: function(json){
      // We've failed on a page. Return nothing
      return null;
    }
  });  
}

/* ****************************************************************************
Periodically check the membership of the moderation list ...
******************************************************************************/
TweetFetcher.prototype.updateModerationList = function(members, cursor) {
  this.debug("updateModerationList", this.settings.moderation_list);

  if (!this.settings.moderation_list) return;

  this.getMembersOfList(null, -1);
  
  var that = this;
  setTimeout( (function(){
    that.updateModerationList();
  }), that.settings.moderation_freq); 

}

/* ****************************************************************************
Get the remaining calls left on the API
TODO: Use this to decrease the calls to the moderation list. As the list gets
longer, it uses more calls so frequency should decrease.
******************************************************************************/
TweetFetcher.prototype.getAPILimit = function() {
  var fetcher = this; 
  
  var requestURL = this.apiEndpoint + "/account/rate_limit_status.json";

  $.ajax({
    url: requestURL,
    type: 'GET',
    dataType: 'jsonp',
    timeout: 10000,
    success: function(json){
      fetcher.apiCallsRemaining = json.remaining_hits;  
      if (fetcher.apiCallsRemaining <= fetcher.settings.api_limit_cutoff) {
        this.attemptRESTCalls = false;
        fetcher.settings.api_limit_exceeded();
      } else {
        fetcher.attemptRESTCalls = true;
        fetcher.settings.api_limit_okay();    
      }

    },
    error: function (xhr, ajaxOptions, thrownError){
        fetcher.debug("getAPILimit", "Error: " + xhr.status);
    },
    complete: function(jqXHR, textStatus) {

      // We know our initial API status. Start fetching
      if (fetcher.firstAPICheck) {
        fetcher.firstAPICheck = false;

        // Start updating the moderation list
        if (fetcher.settings.moderating && fetcher.settings.moderation_list) {
          fetcher.debug("getAPILimit", "Starting the moderator");     
          fetcher.updateModerationList(); 
        }

        // Start fetching tweets
        fetcher.debug("getAPILimit", "Starting the fetcher");
        fetcher.fetchTweets();
      }     

    }
    });  
  
  var that = this;
  setTimeout( (function(){
    that.getAPILimit();
  }), that.settings.api_limit_freq);  

}

/* ****************************************************************************
Get offline tweet 
******************************************************************************/
TweetFetcher.prototype.offlineTweetList = function() { 
  var datestring = new Date();
  return {"results":[
      {"created_at": datestring ,"from_user": this.settings.offline_from_user, "text": this.settings.offline_text}
    ]};           

}

/* ****************************************************************************
Filter the tweets if moderating
******************************************************************************/
TweetFetcher.prototype.moderate = function(tweetList, moderation_list) { 
  if (!this.settings.moderating) return tweetList;
  if (!moderation_list || moderation_list.length == 0) return this.emptyResults;
  
  for(var i = tweetList.results.length -1; i>=0; i--) {
    var this_user = tweetList.results[i].from_user;
    if (!arrayContains(moderation_list, this_user)) {
      if (!arrayContains(this.rejectedTweeters, this_user)) this.rejectedTweeters.push(this_user);
      tweetList.results.splice(i,1);                
    }
  }

  return tweetList;

}

/* ****************************************************************************
Fetch the next batch of tweets
******************************************************************************/
TweetFetcher.prototype.fetchTweets = function() { 

  var fetcher = this; 
  var requestUrl = this.searchEndpoint + "/search.json?q="+ escape(this.settings.query) +"&rpp="+this.settings.fetch_number+"&lang=en";
  this.debug("fetchTweets", requestUrl);

  $.ajax({
    url: requestUrl,
    type: 'GET',
    dataType: 'jsonp',
    timeout: 10000,
    success: function(json){
      if (json.results) {
        fetcher.tweetList = fetcher.moderate(json, fetcher.moderationMembers);  
        fetcher.debug("fetchTweets", "Got new " + json.results.length + " moderated tweets");
        fetcher.settings.search_limit_okay();
  

      } else {
        // No results. Rate limited?
        fetcher.debug("fetchTweets", "No results. Rate limited on Fetch?");
        fetcher.settings.search_limit_exceeded();
      }
      
      // Our first list of tweets. Start filling slots
      if (fetcher.firstTweetList) {
        fetcher.firstTweetList = false;
        fetcher.fillSlot();
      }
      
      if (fetcher.settings.on_success) fetcher.settings.on_success();
    },
    error: function() {
      // If we've failed and don't have any, set a default and call the success function
      if (!fetcher.tweetList || !fetcher.tweetList.results || fetcher.tweetList.results.length == 0) {
        var datestring = new Date();
        fetcher.tweetList = fetcher.offlineTweetList();           
        if (fetcher.settings.on_success) fetcher.settings.on_success();
      }
    }
  });
  
  
  var that = this;
  setTimeout( (function(){
    that.fetchTweets();
  }), that.settings.fetch_freq);
  
}

/* ****************************************************************************
Fills the next available slot with a tweet
******************************************************************************/
// Fill the next slot    
TweetFetcher.prototype.fillSlot = function() {
  if (!this.tweetList) return;

  var slotsToFill = this.settings.slots;

  if (!slotsToFill) return;

  if (this.nextIndex >= this.tweetList.results.length) this.nextIndex = 0;

  var f = $('#' + slotsToFill[this.nextToFill]);
  if (!f) {
    alert('No item with ID ' + slotsToFill[this.nextToFill]);
    return;
  }

  var tweet = this.tweetList.results[this.nextIndex];
  
  if (tweet) (this.settings.create_tweet_element(f, tweet));

  this.nextToFill++;
  
  if (this.nextToFill >= slotsToFill.length) {
    this.nextToFill = 0;
    
    if (this.firstCycle) {
      // We've just filled them all up. Start the timer   
      this.firstCycle = false;
      var that = this;
      setInterval( (function(){
        that.fillSlot();
      }), that.settings.tweet_cycle_freq);      
    }
    
  }
  
  this.nextToFill %= slotsToFill.length;
  this.nextIndex++;

  // If we're just starting, fill all of them     
  if (this.firstCycle) {
    this.fillSlot();
  }
}

/* ****************************************************************************
 From twitter's own script - relative time calculator
 @param {string} twitter date string returned from Twitter API
 @return {string} relative time like "2 minutes ago" 
******************************************************************************/
function timeAgo(dateString) {
    var rightNow = new Date();
    var then = new Date(dateString);
    
    var diff = rightNow - then;
    
    var second = 1000,
    minute = second * 60,
    hour = minute * 60,
    day = hour * 24,
    week = day * 7;
    
    if (isNaN(diff) || diff < 0) {
        return ""; // return blank string if unknown
    }
    
    if (diff < second * 2) {
        // within 2 seconds
        return "right now";
    }
    
    if (diff < minute) {
        return Math.floor(diff / second) + " seconds ago";
    }
    
    if (diff < minute * 2) {
        return "about 1 minute ago";
    }
    
    if (diff < hour) {
        return Math.floor(diff / minute) + " minutes ago";
    }
    
    if (diff < hour * 2) {
        return "about 1 hour ago";
    }
    
    if (diff < day) {
        return  Math.floor(diff / hour) + " hours ago";
    }
    
    if (diff > day && diff < day * 2) {
        return "yesterday";
    }
    
    if (diff < day * 365) {
        return Math.floor(diff / day) + " days ago";
    }
    
    else {
        return "over a year ago";
    }
    
};
 
 
/* ****************************************************************************
  Helpers
******************************************************************************/
function arrayContains(a, obj) {
  var i = a.length;
  while (i--) {
  if (a[i] === obj) {
    return true;
  }
  }
  return false;
}

$(document).ready(function() {
  var tweeter;

  function startUpdating() {
    // Tweeter configuration
    tweeter = new TweetFetcher({
      query: 'kaldorgroup',
      slots: ["tweets"],
      create_tweet_element: function(el, tweet) {
        var t = tweet.text + " - <strong>" + tweet.from_user + "</strong>, " + timeAgo(tweet.created_at);
        el.html(t);
      },
      api_limit_exceeded: function() {},
      api_limit_okay: function() {},  
      search_limit_exceeded: function() {},
      search_limit_okay: function() {}     
    });
  }

  startUpdating();
});
 
