
/* ************************************************************************************* */
// data model

// nodes created by extending returned data
var friends = {};   // id -> friend data {id, name, ...}
var clusters = {};  // id -> group members [id, ...]

var myself;
var nodes = []; // profiles
var links = []; // friendships

// for graphical simulation
var force;
var vis;

var globals = {
    'node_user_weight': 1,
    'node_cluster_weight': 0.00001,
    'link_distance': 0,
    'link_strength': 0.2,
    'gravity': 0.6,
    'charge_const': 80000, // divided by number of friends
    'radius': 3,
    'friction': 0.8, // actually 1 - friction coeff
    'theta': 0.9 // higher for more approximation
    };

/* ************************************************************************************* */
/* requests */

function query_friend(total_count, callback) {
    query_friend.count = 0;     // number of all queries (2x friends)
    query_friend.run_count = 0; // number of friend queries

    return function(obj) {

	// load profile
	FB.api( '/' + obj.id, function (r) {
		    if (r.error !== undefined) {
			log(r.error.type + ': ' + r.error.message);
			query_friend.count++;
			query_friend.run_count++;
			return;
		    }

		    friends[r.id] = r;

		    if (r.location !== undefined)
			add_to_cluster(r.id, r.location.id);
		    if (r.hometown !== undefined)
			add_to_cluster(r.id, r.hometown.id);
		    if (r.work !== undefined)
			$.each(r.work, function(i,v) { add_to_cluster(r.id, v.employer.id); });
		    if (r.education !== undefined)
			$.each(r.education, function(i,v) { add_to_cluster(r.id, v.school.id); });

		    query_friend.count++;
		    query_friend.run_count++; // friends loaded, used for display
		    log('Loading ', query_friend.run_count , ' friends...');
		    if (query_friend.count == total_count * 3)
			callback();
		});

	// load mutual friends
	FB.api( '/' + FB._userID + '/mutualfriends/' + obj.id, function(r) {
		    if (r.error !== undefined) {
			log(r.error.type + ': ' + r.error.message);
			return;
		    }

		    var i;
		    for (i=0; i<r.data.length; i++) {
			links.push( {"source": obj.id, "target": r.data[i].id});
			// this gets converted into adjacency matrix format later
			// TODO: directly construct adjacency matrix
		    }

		    query_friend.count++;
		    if (query_friend.count == total_count * 3)
			callback();
		});

	// load groups
	FB.api( '/' + obj.id + '/groups', function(r) {
		    if (r.error !== undefined) {
			log(r.error.type + ': ' + r.error.message);
			return;
		    }

		    var i;
		    for (i=0; i<r.data.length; i++)
			if (r.data[i].version > 0) add_to_cluster(obj.id, r.data[i].id);

		    query_friend.count++;
		    if (query_friend.count == total_count * 3)
			callback();
		});
    };
}

function query_group(total_count, callback, type) {
    return function(origin, obj) {
	switch(type) {

	case 'groups':
	    if (obj.version > 0) {
		add_to_cluster(origin.id, obj.id);
	    }

	    query_group[type].count++;

	    if (query_group[type].count == total_count) {
		callback();
	    }

	    break;

	case 'friendlists':
	    FB.api( '/' + obj.id + '/members', function (r) {
			if (r.error !== undefined) {
			    log(r.error.type + ': ' + r.error.message);
			    return;
			}

			var i;
			if (r.list_type == 'acquaintances') {
			    for (i in r.data) {
				friends[r.data[i].id].acquaintance = true;
			    }
			}
			else if (r.list_type == 'close_friends') {
			    for (i in r.data) {
				friends[r.data[i].id].closefriend = true;
			    }
			}
			else {
			    for (i=0; i<r.data.length; i++)
				add_to_cluster(r.data[i].id, obj.id);
			}
			query_group[type].count++;
			if (query_group[type].count == total_count) callback();
		    });
	    break;

	default:
	    alert('An error occurred. We should never get here.');
	}
    };
}

function add_to_cluster(node_id, cluster_id) {
    if (clusters[cluster_id] === undefined)
	clusters[cluster_id] = [];
    clusters[cluster_id].push(node_id);
}

/* ************************************************************************************* */
/* visualization */

function refresh_canvas() {
    vis.selectAll('line.link').
	attr('x1', function(d) { return d.source.x; }).
	attr('y1', function(d) { return d.source.y; }).
	attr('x2', function(d) { return d.target.x; }).
	attr('y2', function(d) { return d.target.y; }).
	data(links).

    enter().insert('svg:line').
	attr('class', 'link').
	attr('x1', function(d) { return d.source.x; }).
	attr('y1', function(d) { return d.source.y; }).
	attr('x2', function(d) { return d.target.x; }).
	attr('y2', function(d) { return d.target.y; }).

    filter(function(d) { return d.source.type === 'cluster' | d.target.type === 'cluster'; }).
	attr('class', 'link link-hidden');

    vis.selectAll('circle.node').
	attr('cx', function (d) { if (d.xfixed !== undefined) d.x = d.xfixed; return d.x; } ).
	attr('cy', function (d) { if (d.yfixed !== undefined) d.y = d.yfixed; return d.y; } ).
	data(nodes).

    style('fill', function(d) {
	     return d.filter === undefined ? 'white' : d.filter;
	 }).

    on("mouseover", function(e) {
	   profile_panel(e);
	   d3.select(this).
	       style("stroke", "#ff0000");
       }).

    on("mouseout", function(e) {
	   d3.select(this).
	       style("stroke", "none");
       }).

    enter().insert('svg:circle').
	attr('class', 'node').
	attr('cx', function(d) { return d.xfixed || d.x; }).
	attr('cy', function(d) { return d.yfixed || d.y; }).
	attr('r', globals.radius).

    filter(function(d) { return d.type === 'cluster'; }).
	attr('class', 'node node-hidden');

//    force.start();
}

/* ************************************************************************************* */
/* helpers */

function relax_node(n) {
    nodes[n].x = nodes[n].xfixed || nodes[n].x;
    nodes[n].y = nodes[n].yfixed || nodes[n].y;
    nodes[n].xfixed = undefined;
    nodes[n].yfixed = undefined;
}

function log(beginning, middle, end) {
    if (log.beginning !== beginning || log.end !== end) {
	log.beginning = beginning;
	log.end = end;
	log.prevHTML = document.getElementById('log').innerHTML +
	    (document.getElementById('log').innerHTML == '' ? '' : '<br/>');
    }
    document.getElementById('log').innerHTML = log.prevHTML + beginning + (middle || '') + (end || '');
}

function network_panel() {
    $('#filter').children().detach();

    $('#filter')
	.append('<div>Nodes: ' + nodes.length + '</div>')
	.append('<div>Links: ' + links.length + '</div>');
}

function profile_panel(node) {
    if (typeof(node) == undefined) return;

    $('#profile').children().detach();

    $('#profile')
	.append('<div class="picture"><img src="https://graph.facebook.com/' + node.id + '/picture" /></div>')
	.append('<div class="name">' + node.first_name + ' ' + node.last_name + '</div>')
	.append('<div class="domains"></div>');
    $('#profile .pic, #profile .name')
	.wrap('<a href="' + node.link + '" />');

    if (node.location !== undefined)
	$('#profile .domains').append('<div class="loc">' +
				      '<div class="caption">Lives in</div>' +
				      '<a href="#" onclick="filter_cluster(' + node.location.id + ')">' +
				      node.location.name + '</a></div>');
    if (node.hometown !== undefined)
	$('#profile .domains').append('<div class="from">' +
				      '<div class="caption">From</div>' +
				      '<a href="#" onclick="filter_cluster(' + node.hometown.id + ')">' +
				      node.hometown.name + '</a></div>');
    if (node.work !== undefined)
	$.each(node.work, function(i,v) { $('#profile .domains')
					  .append('<div class="work"><div class="caption">Work</div>' +
						  (v.position !== undefined ? v.position.name + ' at ' : '') +
						  '<a href="#" onclick="filter_cluster(' + v.employer.id + ')">' +
						  v.employer.name +
						  '</a></div>');
					});
    if (node.education !== undefined)
	$.each(node.education, function(i,v) { $('#profile .domains')
					       .append('<div class="edu"><div class="caption">' + v.type + '</div>' +
						       (v.school !== undefined ? '<a href="#" onclick="filter_cluster(' + v.school.id + ')">' + v.school.name : '') + '</a>' +
						       (v.year !== undefined ? ', ' + v.year.name : '') +
						       '</div>');
					     });
    //'significant_other'

}

function filter_cluster(id) {
    var i;
    var col = (Math.random() < 0.5) ? 'blue' : 'red'; //TODO: draw from spectrum

    if (filter_cluster.prev !== undefined && clusters.hasOwnProperty(filter_cluster.prev))
	for (i=0; i<clusters[filter_cluster.prev].length; i++) {
	    friends[clusters[filter_cluster.prev][i]].filter = '';
	}

    if (clusters.hasOwnProperty(id))
	for (i=0; i<clusters[id].length; i++) {
	    friends[clusters[id][i]].filter = col;
	}
    filter_cluster.prev = id;

}

/* ************************************************************************************* */
/* application */

function app_init() {
    init_network();
}

function init_network() {
    log('Loading friend list...', null, null);

    FB.api( '/me', function (r) {
		if (r.error !== undefined) {
		    log(r.error.type + ': ' + r.error.message);
		    return;
		}

		$('#logout').before('Logged in as ' + r.first_name + ' ' + r.last_name + ' &#183; ');
		myself = r;
		friends[r.id] = r;
	    });

    FB.api( '/me/friends',
	    function(r) {
		if (r.error !== undefined) {
		    log(r.error.type + ': ' + r.error.message);
		    return;
		}

		var i;
		if (!r || r.error || !r.data)
		    log('Log in to Facebook.', null, null);
		else
		    for (i=0; i<r.data.length; i++)
			query_friend(r.data.length, init_cluster)(r.data[i]);
	    }); // TODO: batch requests (50 at a time)
}

function init_cluster() {
    // static clustering (based on topology)
    var cluster_queries = ['friendlists', 'groups'];
    if (init_cluster.runs === undefined) init_cluster.runs = 0;
    if (init_cluster.runs === cluster_queries.length)  {
	init_canvas();
	return;
    }

    console.log('Loading ' + cluster_queries[init_cluster.runs]);
    FB.api( '/me/' + cluster_queries[init_cluster.runs], function(type) {
		query_group[type] = {'count': 0};
		return function (r) {
		    if (r.error !== undefined) {
			log(r.error.type + ': ' + r.error.message);
			return;
		    }

		    var j;
		    for (j=0; j<r.data.length; j++) {
			log('Loaded ', j + 1, ' ' + type + '...');
			query_group(r.data.length, init_cluster, type)(myself, r.data[j]);
		    }
		    myself.myself = true;
		    if (r.data.length == 0) init_cluster();
		};
	    }(cluster_queries[init_cluster.runs]));
    init_cluster.runs++;
}

function init_canvas() {
    log('Initializing layout...');
    var c = $('#canvas');
    w = c.width(), h = c.height();
    vis = d3.select('#canvas').
	append('svg:svg').
	attr('width', w).
	attr('height', h);

    // static layout code begins here

    // cores: determine displacement of each node from center
    var static_cores = [[],[],[]];
    for (i in friends) {
	if (friends[i].close_friend == true)
	    friends[i].core = 1;
	else if (friends[i].acquaintance == true)
	    friends[i].core = 3;
	else
	    friends[i].core = 2;
	static_cores[friends[i].core].push(i);
    }

    // construct adjacency matrix
    var links_matrix = {};
    for (i in friends) {
	//if (friends[i]['id'] === undefined)
	//    console.log('Corrupt profile: '+i);
	links_matrix[friends[i]['id']] = [];
    }
    for (i in links) {
	//if (links[i]['source'] === undefined || links[i]['target'] === undefined)
	//    console.log('Corrupt connection: '+i);
	links_matrix[links[i].source].push(links[i]['target']);
	links_matrix[links[i].target].push(links[i]['source']);
    }

    // Brandes' algorithm
    debugger;
    var cen = betweenness_centrality(friends, links_matrix, function(c) {
					 console.log('Clustering '+ c.toString()+ ' friends...');
				     });

    // static layout: determine coordinates on canvas
    var r = (Math.min(w,h) * .5);
    for (core in static_cores) {

	var core_length = static_cores[core].length;
	var radius = r*Math.sqrt(core/static_cores.length);

	$.each(static_cores[core], function(i,o) {
		   var node = friends[o];
		   var turns = 2*Math.PI*i/core_length;
		   var radius_adj = radius + (i&1 ? 10 : -10);

		   node.xfixed = w*.5 - (radius_adj) * Math.cos(turns);
		   node.yfixed = h*.5 - (radius_adj) * Math.sin(turns);
		   node.fixed = true;

		   node.weight = globals.node_user_weight;
		   nodes.push(node);
	   });
    }

    /*
    // cluster by introducing nodes for groups
    for (i in clusters) {
	if (clusters[i].length == 1) continue;

	var node = {'type': 'cluster', 'id': i};

	node.x = (Math.random()/8 + .5) * w;
	node.y = (Math.random()/8 + .5) * h;

	node.weight = globals.node_cluster_weight;
	nodes.push(node);

	for (j in clusters[i]) {
	    if (friends[clusters[i][j]] !== undefined)
		links.push({source: friends[clusters[i][j]], target: node, type: 'group'});
	}
    }
     */

    // set up for the force directed layout
    for (i in links) {
	links[i].source = friends[links[i].source];
	links[i].target = friends[links[i].target];
    }

    force = d3.layout.force().
	nodes(nodes).
	links(links).
	size([w,h]).
	linkDistance(globals.link_distance).
	linkStrength(globals.link_strength).
	charge(-globals.charge_const/nodes.length).
	gravity(globals.gravity).
	friction(globals.friction).
	theta(globals.theta).
    on('tick', refresh_canvas).
	start();

    // collapse nodes with high betweenness
    for (i in cen)
	if (cen[i] > 0)
	    relax_node(i);

    refresh_canvas();

    // user interface controls
    $('.playBtn').hide();
    $('.pauseBtn').show();
    $('.playBtn a').click(function() {
			      force.resume();
			      $('.playBtn').hide();
			      $('.pauseBtn').show();
			  });
    $('.pauseBtn a').click(function() {
			       force.stop();
			       $('.pauseBtn').hide();
			       $('.playBtn').show();
			   });

    profile_panel(myself);
    network_panel(); // display network statistics
}

