var config = {
   proxy: "/proxy/cache/",
   max_nodes: 40,
   suggestions: ["@twitter/team", "@rtkrum/cool-infographics-people", 
         "@jayrosen_nyu/best-mindcasters-i-know", "@libbey/apple-tablet"],
   debug: true
}

if (document.location.href.indexOf("file://") == 0)
   config.proxy = "http://tx.mememapper.com/proxy/cache/";

var T0 = new Date;
var MAX_NODES = config.max_nodes;
var DEBUG = config.debug;

var data = {};
var network, nodes, edges;
var tabs, absoluteSize;

if (typeof console === "undefined") {
   var console = {
      log: new Function
   }
}

Date.getText = function(str) {
   // The Twitter API returns two different date string formats, either
   // "Tue May 11 17:08:39 +0000 2010" or "Tue, 11 May 2010 04:16:42 +0000";
   // thus, we need to fix the date string for MSIE before parsing it.
   if ($.browser.msie) {
      str = str.replace(new RegExp("^(\\S* \\S* \\S*)(.*)([^+]\\d{4})$"), "$1 $3$2");
   }
   var date = Date.parse(str);
   var delta = new Date - date;

   var diffs = {};
   diffs.minutes = parseInt(delta / 1000 / 60);
   diffs.hours = parseInt(diffs.minutes / 60);
   diffs.days = parseInt(diffs.hours / 24);

   var periods = ["day", "hour", "minute"];
   for (var i=0, period, diff; i<periods.length; i+=1) {
      period = periods[i];
      diff = diffs[period + "s"];
      if (diff > 0) {
         return diff + " " + period + (diff > 1 ? "s" : "") + " ago";
      }
   }
   return "Just a moment ago";
}

$.fn.render = function(name, data, replace) {
   return $(this)[replace ? "html" : "append"](render(name, data));
}

$.fn.loading = function(text) {
   return $(this).empty().render("loading-template", {text: text || ""});
}

$(function() {
   network = new Network;

   // Bad, bad, bad work-around for Safari and Chrome to 
   // correctly align the applet container with the tabs:
   $.browser.webkit && $(".yui-g").css("margin-top", 24);
   
   // Setup tabs
   tabs = new YAHOO.widget.TabView("tabs");
   tabs.getTab(3).addListener("click", function() {
      updateAboutTab();
   });
   
   // Setup initial display
   updateAboutTab();
   $("a[href=?]").css("color", "red"); // FIXME: Add mising URLs
   
   // Activate the suggest link
   /*
   $("#suggest").click(function() {
      var index = $.data(this, "index") || 0;
      index >= config.suggestions.length && (index = 0);
      $("#list-input input:text").val(config.suggestions[index]);
      $.data(this, "index", ++index);
      return false;
   });
   */
   
   

   var input = $("#list-input input:text").val();
   $("#list-input").submit(function() {
      try {
         network.destroy();
         network.loading(" – this might take a while");
         var input = $(this).find(":text").val()
               .replace("http://twitter.com/", "").replace(/^@/, "");
         if (input !== location.hash) {
            location.hash = input;
         }
         nodes = [];
         edges = [];
         document.getElementById("tweet").href = "http://twitter.com/home?status=" + encodeURIComponent("Visualizing the conversational network of @" + input + ": " + document.location.href);
         retrieveListMessages(input, function(type, nodes) {
            if (type === "finalize") {
               absoluteSize = nodes.length;
               network.onLoad(function() {
                  createNetwork(nodes);
                  // Tell the applet that all data has been transmitted and it can safely start to draw the network.
                  // Hopefully, this fixes the “array index out of bounds“ errors...
                  network.call("enableDrawing");
                  updateNetworkTab(input);
               });
               network.load();
               return;
            } else if (type === "error") {
               network.error(nodes);
            }
         });
      } catch (ex) {
         debug(ex);
      }
      return false;
   });
   if (location.hash !== input) {
      $("#list-input :text").val(location.hash.substr(1));
      $("#list-input").submit();
   }
   return;
   ///////
   $.ajax({
      url: "../data/pajek-example.net",
      dataType: "text",
      success: function(data) {
         var pajek = parsePajek(data);
         debug(pajek);
      }
   });
   return;
});

var Network = function() {
   var isLoaded = false;
   var container = $("#applet");
   var callback = new Function;
   
   var getApplet = function() {
      return container.find("object")[0];
   }

   this.onLoad = function(func) {
      if (func) {
         func instanceof Function && (callback = func);
      } else {
         debug("Applet called onLoad handler");
         isLoaded = true;
         callback.call(this);
      }
      return;
   }
   
   this.load = function() {
      if (!isLoaded) {
         container.empty().render("applet-template");
      }
      return isLoaded;
   }
   
   this.loading = function() {
      return container.loading.apply(container, arguments);
   }
   
   this.isLoaded = function() {
      return isLoaded;
   }

   this.destroy = function() {
      if (isLoaded) {
         container.empty();
         isLoaded = false;
      }
      return !isLoaded;
   }
   
   this.error = function(msg) {
      container.empty().render("error-template", {
         text: String(msg)
      });
      return;
   }
   
   this.call = function(method /*, arg1, arg2, ... */) {
      if (isLoaded) {
         var applet = getApplet();
         var args = Array.prototype.slice.call(arguments, 1);
         if ($.browser.msie) {
            switch (method) {
               case "reset":
               return eval(String(applet.reset()));
               case "enableDrawing":
               return eval(String(applet.enableDrawing()));
               case "addEdge":
               return eval(String(applet.addEdge(args[0], args[1], args[2])));
               default:
               return alert(method + " not defined!");
            }
         }
         return eval(String(applet[method].apply(applet, args)));
      }
      return false;
   }
   
   this.reset = function() {
      return isLoaded && this.call("reset");
   }
   
   this.connect = function(node1, node2, size) {
      return isLoaded && this.call("addEdge", node1, node2, size);
   }
   
   return this;
}

var Node = function(name) {
   this.name = name;
   this.outgoing = [];
   this.incoming = [];
   this.connectWith = function(node) {
      var edge = Edge.add(this, node);
   }
   return this;
}

Node.add = function(name) {
   var id = "id:" + name;
   var node = nodes[id];
   if (!node) {
      node = nodes[id] = new Node(name);
      nodes.push(node);
   } else {
      debug("Redundant node: " + name);
   }
   return node;
}

Node.get = function(name) {
   return nodes["id:" + name];
}

var Edge = function(node1, node2, size) {
   this.oneNode = node1;
   this.otherNode = node2;
   this.size = size || 0;
   return this;
}

Edge.add = function(node1, node2) {
   var edge = Edge.get(node1, node2);
   if (!edge) {
      edge = edges[node1.name + ">" + node2.name] = new Edge(node1, node2);
      edges.push(edge);
      node1.outgoing.push(edge);
      node2.incoming.push(edge);
   }
   edge.size += 1;
   return edge;
}

Edge.get = function(node1, node2) {
   return edges[node1.name + ">" + node2.name];
}

var debug = function(str, timestamp) {
   if (DEBUG) {
      console.log("***** " + (timestamp ? "T+" + getTime() + ": " : "") + str);
   }
}

var getTime = function() {
   return (Date.now() - T0) + "ms";
}

var createNetwork = function(nodes) {
   nodes.sort(function(a, b) {
      return b.incoming.length + b.outgoing.length - 
            (a.incoming.length + a.outgoing.length);
   });
   nodes.length = Math.min(MAX_NODES, nodes.length);
   var vertices = "";
   var arcs = "";
   $.each(nodes, function(index, node) {
      if (node.incoming.length + node.outgoing.length > 0) {
         debug(node.name, node.incoming.length, node.outgoing.length);
         vertices += index+1 + ' "' + node.name + '" ' + node.incoming.length + 
               " " + node.outgoing.length + " 0\n";
         $.each(node.outgoing, function(index, edge) {
            var index2;
            if ((index2 = $.inArray(edge.otherNode, nodes)) > -1) {
               debug(" +- " + edge.otherNode.name);
               arcs += $.inArray(node, nodes)+1 + " " + (index2+1) + " " + 
                     edge.size + "\n";
               network.connect(node.name, edge.otherNode.name, edge.size);
            }
         });
      }
   });
   debug("*Vertices " + nodes.length + "\n" + vertices + "*Arcs\n" + arcs);
   return;                          
}

function render(name, data) {
   var regExp;
   var template = '<div class="' + name + '">' + $("script#" + name).html() + "</div>";
   for (var key in data) {
      regExp = new RegExp("{" + key + "}", "gi");
      template = template.replace(regExp, data[key] || "");
   }
   return template.replace(new RegExp("{[^}]*}", "gi"), "?");
}

/**
 * Source: http://blog.stevenlevithan.com/archives/faster-trim-javascript
 */
function trim(str) {
   var str = str.replace(/^\s\s*/, ''),
         ws = /\s/,
         i = str.length;
   while (ws.test(str.charAt(--i)));
   return str.slice(0, i + 1);
}

var parsePajek = function(data) {
   var pajek = {nodes: [], edges: []};

   var mode;
   var lines = data.split(/\n\r|\n|\r/g);
   for (var i=0, line; i<lines.length; i+=1) {
      line = trim(lines[i]);
      if (line.length === 0) {
         continue;
      }
      if (line.indexOf("*") === 0) {
         var section = line.replace(/\*(\S*).*$/, "$1").toLowerCase();
         if (section === parsePajek.VERTICES) {
            mode = parsePajek.VERTICES;
         } else if (section === parsePajek.ARCS) {
            mode = parsePajek.ARCS;
         }
      } else if (mode === parsePajek.VERTICES) {
         var parts = line.split(/\s+/g);
         var i = parseInt(parts[0]);
         pajek.nodes[i-1] = {
            index: i,
            id: trim(parts[1]).replace(/"/g, ""),
            x: trim(parts[2]),
            y: trim(parts[3]),
            z: trim(parts[4])
         }
      } else if (mode === parsePajek.ARCS) {
         var parts = line.split(/\s+/g);
         pajek.edges.push({
            oneNode: parseInt(parts[0] - 1),
            otherNode: parseInt(parts[1] - 1),
            size: parseInt(parts[2])
         });
      }
   }

   return pajek;
}
   
parsePajek.VERTICES = "vertices";
parsePajek.ARCS = "arcs";

function birdcall(data, callback, errorCallback) {
   var url;
   var baseUrl = config.proxy + "twitter.com/";
   var searchUrl = config.proxy + "search.twitter.com/search.json?result_type=recent&rpp=";

   switch (data.type) {
      case "profile":
      url = baseUrl + "users/show.json?screen_name=" + data.user;
      break;
   
      case "messages":
      url = searchUrl + data.limit + "&from=" + data.user;
      break;
   
      case "conversations":
      url = searchUrl + data.limit + "&from=" + data.from + "&to=" + data.to;
      break;
      
      case "status":
      url = config.proxy + "api.twitter.com/1/statuses/show.json?id=" + data.id;
      break;
   }
   $.ajax({
      url: url,
      dataType: "json",
      success: function(data) {
         callback(data);
         debug(url);
      },
      error: function(data) {
         errorCallback && errorCallback(data);
         debug(url);
      }
   });
}

function updateAboutTab() {
   $("#about").empty().render("about-template");
   $("#applet").empty().render("help-template");
   $.each(config.suggestions, function(index, item) {
      var link = $("<a/>").attr({
         "class": "live",
         "href": item.replace("@", "#")
      }).html(item);
      $("#suggestions").append($("<li/>").append(link));
   });
   $("a.live").click(function() {
      $("#list-input input:text").val($(this).attr("href").replace("#", "")).submit();
      return false;
   });
}

function updateNetworkTab(name) {
   data.nodes = getNodes();
   
   if (edges.length < 1) {
      $("#network").empty().render("no-network-template", {name: name});
      tabs.set("activeIndex", 0);
      return;
   }

   data.rankings = getRankings();
   
   var param = {
      name: name,
      incoming_ranking: (function() {
         var item, ranking = {};
         for (var i = 0; i < 3; i += 1) {
            item = data.rankings.incoming[i];
            ranking["name" + i] = item[0];
            ranking["count" + i] = item[1];
         }
         return render("ranking-template", ranking);
      })(),
      outgoing_ranking: (function(){
         var item, ranking = {};
         for (var i = 0; i < 3; i += 1) {
            item = data.rankings.outgoing[i];
            ranking["name" + i] = item[0];
            ranking["count" + i] = item[1];
         }
         return render("ranking-template", ranking);
      })()
   };
   
   param.top = absoluteSize > MAX_NODES ? MAX_NODES : "";
   var kind = (name || "").indexOf("/") >= 0 ? "list" : "user";
   param.explanation = render("network-explanation-" + kind + "-template", param);
   $("#network").empty().render("network-template", param);
   tabs.set("activeIndex", 0);
   return undefined;
}

function updateProfileTab(user) {
   var messages = "";
   var profile = $("#profile").loading();

   birdcall({type: "messages", user: user, limit: 3}, function(data) {
      //var template = $("#profile .message.template");
      var message;
      for (var i=0; i<data.results.length; i+=1) {
         message = data.results[i];
         message.text = plainText(message.text);
         message.created_at = Date.getText(message.created_at);
         message.source = plainText(message.source || "");
         if (message.to_user) {
            message.source += " in reply to " + message.to_user;
         }
         message.url = "http://twitter.com/" + message.from_user + "/status/" + message.id;
         messages += render("message-template", message);
      }
      
      birdcall({type: "profile", user: user}, function(data) {
         var ranking = getUserRanking(data.screen_name.toLowerCase());
         if (ranking.length) {
            data.incoming = ranking[0].messages;
            data.outgoing = ranking[1].messages;
            data.total = ranking[2].messages;
            data.incomingRank = ranking[0].rank;
            data.outgoingRank = ranking[1].rank;
            data.totalRank = ranking[2].rank;
         }
         data.messages = messages;
         profile.empty().render("profile-template", data);
      });
   
   });
   
   tabs.set("activeIndex", 1);
   return undefined;
}

function updateConversationTab(user1, user2) {
   var item, param = {messages: ""}, messages = [];
   var conversation = $("#conversation").loading();

   birdcall({type: "profile", user: user1}, function(data) {
      data.index = 1;
      param.image1 = render("image-template", data);

      birdcall({type: "profile", user: user2}, function(data) {
         data.index = 2;
         param.image2 = render("image-template", data);

         birdcall({type: "conversations", from: user1, to: user2, limit: 3}, function(data) {
            for (var i in data.results) {
               item = data.results[i];
               messages[messages.length] = item;
            }
      
            birdcall({type: "conversations", from: user2, to: user1, limit: 3}, function(data) {
               for (var i in data.results) {
                  item = data.results[i];
                  messages[messages.length] = item;
               }
               
               addParentTweets(messages, function(messages) {
                  messages.sort(function(a, b) {
                     return new Date(a.created_at) - new Date(b.created_at);
                  });
   
                  for (var i=0; i<messages.length; i+=1) {
                     item = messages[i];
                     item.align = (item.from_user.toLowerCase() == user1 ? "left" : "right");
                     item.created_at = Date.getText(item.created_at);
                     item.source = plainText(item.source);
                     item.url = "http://twitter.com/" + item.from_user + "/status/" + item.id;
                     param.messages += render("message-template", item);
                  }
      
                  conversation.empty().render("conversation-template", param);
               });
            });
         });
      });
   });

   tabs.set("activeIndex", 2);
   return undefined;
}

function addParentTweets(messages, callback) {
   // Currently deactivated
   return callback(messages);

   var counter = messages.length;
   $.each(messages, function(index, item) {
      birdcall({type: "status", id: item.id}, function(data) {
         var id = data.in_reply_to_status_id;
         for (var i in messages) {
            if (messages[i].id === id) {
               id = null;
               break;
            }
         }
         if (id) {
            birdcall({type: "status", id: id}, function(data) {
               // Unifying parent tweets retrieved from status object
               data.user && (data.from_user = data.user.screen_name);
               messages[messages.length] = data;
               counter -= 1;
            }, function() {
               counter -= 1;
            });
         } else {
            counter -= 1;
         }
      }, function() {
         counter -= 1;
      });
   });
   var scheduler = setInterval(function() {
      debug(counter);
      if (counter === 0) {
         clearInterval(scheduler);
         return callback(messages);
      }
   }, 100);
}

function plainText(str) {
    return str.replace(/&lt;/g, "<")
         .replace(/&gt;/g, ">")
         .replace(/&quot;/g, '"')
         .replace(/<[^>]*>/g, "");
}

function getNodes() {
   return nodes;
}

function getRankings() {
   var incoming = [];
   var outgoing = [];
   var total = [];

   // Add named properties containing the 
   // corresponding item and parse its integers
   for (var item, i=0; i<data.nodes.length; i+=1) {
      item = data.nodes[i];
      incoming[i] = [item.name, item.incoming.length];
      outgoing[i] = [item.name, item.outgoing.length];
      total[i] = [item.name, item.incoming.length + item.outgoing.length];
      incoming[item.name] = incoming[i];
      outgoing[item.name] = outgoing[i];
      total[item.name] = total[i];
   }
      
   var sorter = function(a, b) {
      return b[1] - a[1];
   }
   
   incoming.sort(sorter);
   outgoing.sort(sorter);
   total.sort(sorter);
   
   return {
      incoming: incoming,
      outgoing: outgoing,
      total: total
   }
}

function getUserRanking(name) {
   var ranking, result = [];   
   for (var i in data.rankings) {
      ranking = data.rankings[i];
      result[result.length] = {
         messages: ranking[name][1],
         rank: $.inArray(ranking[name], ranking) + 1
      }
   }
   return result;
}

