/* 
 FDEB algorithm implementation [www.win.tue.nl/~dholten/papers/forcebundles_eurovis.pdf].

 Author: Corneliu S. (github.com/upphiminn)
 2013

 */
(function () {
	d3.ForceEdgeBundling = function () {
		var data_nodes = {}, // {'nodeid':{'x':,'y':},..}
			data_edges = [], // [{'source':'nodeid1', 'target':'nodeid2'},..]
			compatibility_list_for_edge = [],
			subdivision_points_for_edge = [],
			K = 0.1, // global bundling constant controlling edge stiffness
			S_initial = 0.1, // init. distance to move points
			P_initial = 1, // init. subdivision number
			P_rate = 2, // subdivision rate increase
			C = 6, // number of cycles to perform
			I_initial = 90, // init. number of iterations for cycle
			I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3
			compatibility_threshold = 0.6,
			eps = 1e-6;


		/*** Geometry Helper Methods ***/
		function vector_dot_product(p, q) {
			return p.x * q.x + p.y * q.y;
		}

		function edge_as_vector(P) {
			return {
				'x': data_nodes[P.target].x - data_nodes[P.source].x,
				'y': data_nodes[P.target].y - data_nodes[P.source].y
			}
		}

		function edge_length(e) {
			// handling nodes that are on the same location, so that K/edge_length != Inf
			if (Math.abs(data_nodes[e.source].x - data_nodes[e.target].x) < eps &&
				Math.abs(data_nodes[e.source].y - data_nodes[e.target].y) < eps) {
				return eps;
			}

			return Math.sqrt(Math.pow(data_nodes[e.source].x - data_nodes[e.target].x, 2) +
				Math.pow(data_nodes[e.source].y - data_nodes[e.target].y, 2));
		}

		function custom_edge_length(e) {
			return Math.sqrt(Math.pow(e.source.x - e.target.x, 2) + Math.pow(e.source.y - e.target.y, 2));
		}

		function edge_midpoint(e) {
			var middle_x = (data_nodes[e.source].x + data_nodes[e.target].x) / 2.0;
			var middle_y = (data_nodes[e.source].y + data_nodes[e.target].y) / 2.0;

			return {
				'x': middle_x,
				'y': middle_y
			};
		}

		function compute_divided_edge_length(e_idx) {
			var length = 0;

			for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {
				var segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]);
				length += segment_length;
			}

			return length;
		}

		function euclidean_distance(p, q) {
			return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2));
		}

		function project_point_on_line(p, Q) {
			var L = Math.sqrt((Q.target.x - Q.source.x) * (Q.target.x - Q.source.x) + (Q.target.y - Q.source.y) * (Q.target.y - Q.source.y));
			var r = ((Q.source.y - p.y) * (Q.source.y - Q.target.y) - (Q.source.x - p.x) * (Q.target.x - Q.source.x)) / (L * L);

			return {
				'x': (Q.source.x + r * (Q.target.x - Q.source.x)),
				'y': (Q.source.y + r * (Q.target.y - Q.source.y))
			};
		}

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

		/*** Initialization Methods ***/
		function initialize_edge_subdivisions() {
			for (var i = 0; i < data_edges.length; i++) {
				if (P_initial === 1) {
					subdivision_points_for_edge[i] = []; //0 subdivisions
				} else {
					subdivision_points_for_edge[i] = [];
					subdivision_points_for_edge[i].push(data_nodes[data_edges[i].source]);
					subdivision_points_for_edge[i].push(data_nodes[data_edges[i].target]);
				}
			}
		}

		function initialize_compatibility_lists() {
			for (var i = 0; i < data_edges.length; i++) {
				compatibility_list_for_edge[i] = []; //0 compatible edges.
			}
		}

		function filter_self_loops(edgelist) {
			var filtered_edge_list = [];

			for (var e = 0; e < edgelist.length; e++) {
				if (data_nodes[edgelist[e].source].x != data_nodes[edgelist[e].target].x ||
					data_nodes[edgelist[e].source].y != data_nodes[edgelist[e].target].y) { //or smaller than eps
					filtered_edge_list.push(edgelist[e]);
				}
			}

			return filtered_edge_list;
		}

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

		/*** Force Calculation Methods ***/
		function apply_spring_force(e_idx, i, kP) {
			var prev = subdivision_points_for_edge[e_idx][i - 1];
			var next = subdivision_points_for_edge[e_idx][i + 1];
			var crnt = subdivision_points_for_edge[e_idx][i];
			var x = prev.x - crnt.x + next.x - crnt.x;
			var y = prev.y - crnt.y + next.y - crnt.y;

			x *= kP;
			y *= kP;

			return {
				'x': x,
				'y': y
			};
		}

		function apply_electrostatic_force(e_idx, i) {
			var sum_of_forces = {
				'x': 0,
				'y': 0
			};
			var compatible_edges_list = compatibility_list_for_edge[e_idx];

			for (var oe = 0; oe < compatible_edges_list.length; oe++) {
				var force = {
					'x': subdivision_points_for_edge[compatible_edges_list[oe]][i].x - subdivision_points_for_edge[e_idx][i].x,
					'y': subdivision_points_for_edge[compatible_edges_list[oe]][i].y - subdivision_points_for_edge[e_idx][i].y
				};

				if ((Math.abs(force.x) > eps) || (Math.abs(force.y) > eps)) {
					var diff = (1 / Math.pow(custom_edge_length({
						'source': subdivision_points_for_edge[compatible_edges_list[oe]][i],
						'target': subdivision_points_for_edge[e_idx][i]
					}), 1));

					sum_of_forces.x += force.x * diff;
					sum_of_forces.y += force.y * diff;
				}
			}

			return sum_of_forces;
		}


		function apply_resulting_forces_on_subdivision_points(e_idx, P, S) {
			var kP = K / (edge_length(data_edges[e_idx]) * (P + 1)); // kP=K/|P|(number of segments), where |P| is the initial length of edge P.
			// (length * (num of sub division pts - 1))
			var resulting_forces_for_subdivision_points = [{
				'x': 0,
				'y': 0
			}];

			for (var i = 1; i < P + 1; i++) { // exclude initial end points of the edge 0 and P+1
				var resulting_force = {
					'x': 0,
					'y': 0
				};

				spring_force = apply_spring_force(e_idx, i, kP);
				electrostatic_force = apply_electrostatic_force(e_idx, i, S);

				resulting_force.x = S * (spring_force.x + electrostatic_force.x);
				resulting_force.y = S * (spring_force.y + electrostatic_force.y);

				resulting_forces_for_subdivision_points.push(resulting_force);
			}

			resulting_forces_for_subdivision_points.push({
				'x': 0,
				'y': 0
			});

			return resulting_forces_for_subdivision_points;
		}

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

		/*** Edge Division Calculation Methods ***/
		function update_edge_divisions(P) {
			for (var e_idx = 0; e_idx < data_edges.length; e_idx++) {
				if (P === 1) {
					subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].source]); // source
					subdivision_points_for_edge[e_idx].push(edge_midpoint(data_edges[e_idx])); // mid point
					subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].target]); // target
				} else {
					var divided_edge_length = compute_divided_edge_length(e_idx);
					var segment_length = divided_edge_length / (P + 1);
					var current_segment_length = segment_length;
					var new_subdivision_points = [];
					new_subdivision_points.push(data_nodes[data_edges[e_idx].source]); //source

					for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {
						var old_segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]);

						while (old_segment_length > current_segment_length) {
							var percent_position = current_segment_length / old_segment_length;
							var new_subdivision_point_x = subdivision_points_for_edge[e_idx][i - 1].x;
							var new_subdivision_point_y = subdivision_points_for_edge[e_idx][i - 1].y;

							new_subdivision_point_x += percent_position * (subdivision_points_for_edge[e_idx][i].x - subdivision_points_for_edge[e_idx][i - 1].x);
							new_subdivision_point_y += percent_position * (subdivision_points_for_edge[e_idx][i].y - subdivision_points_for_edge[e_idx][i - 1].y);
							new_subdivision_points.push({
								'x': new_subdivision_point_x,
								'y': new_subdivision_point_y
							});

							old_segment_length -= current_segment_length;
							current_segment_length = segment_length;
						}
						current_segment_length -= old_segment_length;
					}
					new_subdivision_points.push(data_nodes[data_edges[e_idx].target]); //target
					subdivision_points_for_edge[e_idx] = new_subdivision_points;
				}
			}
		}

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

		/*** Edge compatibility measures ***/
		function angle_compatibility(P, Q) {
			return Math.abs(vector_dot_product(edge_as_vector(P), edge_as_vector(Q)) / (edge_length(P) * edge_length(Q)));
		}

		function scale_compatibility(P, Q) {
			var lavg = (edge_length(P) + edge_length(Q)) / 2.0;
			return 2.0 / (lavg / Math.min(edge_length(P), edge_length(Q)) + Math.max(edge_length(P), edge_length(Q)) / lavg);
		}

		function position_compatibility(P, Q) {
			var lavg = (edge_length(P) + edge_length(Q)) / 2.0;
			var midP = {
				'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,
				'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0
			};
			var midQ = {
				'x': (data_nodes[Q.source].x + data_nodes[Q.target].x) / 2.0,
				'y': (data_nodes[Q.source].y + data_nodes[Q.target].y) / 2.0
			};

			return lavg / (lavg + euclidean_distance(midP, midQ));
		}

		function edge_visibility(P, Q) {
			var I0 = project_point_on_line(data_nodes[Q.source], {
				'source': data_nodes[P.source],
				'target': data_nodes[P.target]
			});
			var I1 = project_point_on_line(data_nodes[Q.target], {
				'source': data_nodes[P.source],
				'target': data_nodes[P.target]
			}); //send actual edge points positions
			var midI = {
				'x': (I0.x + I1.x) / 2.0,
				'y': (I0.y + I1.y) / 2.0
			};
			var midP = {
				'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,
				'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0
			};

			return Math.max(0, 1 - 2 * euclidean_distance(midP, midI) / euclidean_distance(I0, I1));
		}

		function visibility_compatibility(P, Q) {
			return Math.min(edge_visibility(P, Q), edge_visibility(Q, P));
		}

		function compatibility_score(P, Q) {
			return (angle_compatibility(P, Q) * scale_compatibility(P, Q) * position_compatibility(P, Q) * visibility_compatibility(P, Q));
		}

		function are_compatible(P, Q) {
			return (compatibility_score(P, Q) >= compatibility_threshold);
		}

		function compute_compatibility_lists() {
			for (var e = 0; e < data_edges.length - 1; e++) {
				for (var oe = e + 1; oe < data_edges.length; oe++) { // don't want any duplicates
					if (are_compatible(data_edges[e], data_edges[oe])) {
						compatibility_list_for_edge[e].push(oe);
						compatibility_list_for_edge[oe].push(e);
					}
				}
			}
		}

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

		/*** Main Bundling Loop Methods ***/
		var forcebundle = function () {
			var S = S_initial;
			var I = I_initial;
			var P = P_initial;

			initialize_edge_subdivisions();
			initialize_compatibility_lists();
			update_edge_divisions(P);
			compute_compatibility_lists();

			for (var cycle = 0; cycle < C; cycle++) {
				for (var iteration = 0; iteration < I; iteration++) {
					var forces = [];
					for (var edge = 0; edge < data_edges.length; edge++) {
						forces[edge] = apply_resulting_forces_on_subdivision_points(edge, P, S);
					}
					for (var e = 0; e < data_edges.length; e++) {
						for (var i = 0; i < P + 1; i++) {
							subdivision_points_for_edge[e][i].x += forces[e][i].x;
							subdivision_points_for_edge[e][i].y += forces[e][i].y;
						}
					}
				}
				// prepare for next cycle
				S = S / 2;
				P = P * P_rate;
				I = I_rate * I;

				update_edge_divisions(P);
				//console.log('C' + cycle);
				//console.log('P' + P);
				//console.log('S' + S);
			}
			return subdivision_points_for_edge;
		};
		/*** ************************ ***/


		/*** Getters/Setters Methods ***/
		forcebundle.nodes = function (nl) {
			if (arguments.length === 0) {
				return data_nodes;
			} else {
				data_nodes = nl;
			}

			return forcebundle;
		};

		forcebundle.edges = function (ll) {
			if (arguments.length === 0) {
				return data_edges;
			} else {
				data_edges = filter_self_loops(ll); //remove edges to from to the same point
			}

			return forcebundle;
		};

		forcebundle.bundling_stiffness = function (k) {
			if (arguments.length === 0) {
				return K;
			} else {
				K = k;
			}

			return forcebundle;
		};

		forcebundle.step_size = function (step) {
			if (arguments.length === 0) {
				return S_initial;
			} else {
				S_initial = step;
			}

			return forcebundle;
		};

		forcebundle.cycles = function (c) {
			if (arguments.length === 0) {
				return C;
			} else {
				C = c;
			}

			return forcebundle;
		};

		forcebundle.iterations = function (i) {
			if (arguments.length === 0) {
				return I_initial;
			} else {
				I_initial = i;
			}

			return forcebundle;
		};

		forcebundle.iterations_rate = function (i) {
			if (arguments.length === 0) {
				return I_rate;
			} else {
				I_rate = i;
			}

			return forcebundle;
		};

		forcebundle.subdivision_points_seed = function (p) {
			if (arguments.length == 0) {
				return P;
			} else {
				P = p;
			}

			return forcebundle;
		};

		forcebundle.subdivision_rate = function (r) {
			if (arguments.length === 0) {
				return P_rate;
			} else {
				P_rate = r;
			}

			return forcebundle;
		};

		forcebundle.compatibility_threshold = function (t) {
			if (arguments.length === 0) {
				return compatibility_threshold;
			} else {
				compatibility_threshold = t;
			}

			return forcebundle;
		};

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

		return forcebundle;
	}
})();
