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

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

var myself;
var names = [];

// for graphical simulation
var force;
var vis;
// pointers to node and link objects
var nodes = [];
var links = [];

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': 2,
    '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;
    query_friend.run_count = 0;

    return function(obj) {
	FB.api( '/' + obj.id, function (r) {
		    friends[r.id] = r;
		    names[r.id] = r.name;

		    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++;
		    log('Loaded ', query_friend.run_count , ' friends...');
		    if (query_friend.count == total_count * 2)
			callback();
		});

	// cluster by group
	FB.api( '/' + obj.id + '/groups', function(r) {
		    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 * 2)
			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) {
			var i;

			if (obj.list_type == 'acquaintances') {
			    r.core = 5;
			}
			else if (obj.list_type == 'close_friends') {
			    r.core = 1;
			}
			else {
			    r.core = 3;
			    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) { return d.x; } ).
	attr('cy', function (d) { 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.x; }).
	attr('cy', function(d) { return d.y; }).
	attr('r', globals.radius).

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

//    force.start();
}

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

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() {
    // bootstrapped
/*    jQuery.getJSON('json/friends.json', function(r) {
		     friends = r;
		     jQuery.getJSON('json/clusters.json', function(rr) {
				      clusters = rr;
				      init_canvas();
				  });
		 });
*/
    init_network();
}

function init_network() {
    log('Initializing...', null, null);

    FB.api( '/me', function (r) {
		$('#logout').before('Logged in as ' + r.first_name + ' ' + r.last_name + ' &#183; ');
		myself = r;
		friends[r.id] = r;
	    });

    FB.api( '/me/friends',
	    function(r) {
		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) {
		    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.core = 0;
		    if (r.data.length == 0) init_cluster();
		};
	    }(cluster_queries[init_cluster.runs]));
    init_cluster.runs++;
}

function init_canvas() {
    log('Initializing canvas...');
    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: 0: self, 1: close friends, 2: friend lists, 3: friends, 4: other contacts, 5: acquaintances
    // cluster on: friend lists

    // in each inner core, cluster using mutual friends
    // in outer core, iterative (graph-building) clustering? using multiple parameters


    // layout friends
    // add cluster links (without changing positions!?)

    // dynamic layout code begins here
    for (i in friends) {
	var node = friends[i];
	node.type = 'friend';
	node.x = (Math.random()/8 + .5) * w ;
	node.y = (Math.random()/8 + .5) * h ;
	node.weight = globals.node_user_weight;
	nodes.push(node);
    }

    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});
	}
    }

    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', function() { refresh_canvas();
//			    console.log($($('.node')[3]).attr('cx')) ;
//			    console.log($($('.node')[3]).attr('cy')) ;
			  }).
	start();

    $('.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
}

