/*
	Build using:

	uglifyjs javascript/status-page.js -o files/lib/gluon/status-page/www/static/status-page.js -c -m
*/

'use strict';

(function() {
	var _ = JSON.parse(document.body.getAttribute('data-translations'));

	String.prototype.sprintf = function() {
		var i = 0;
		var args = arguments;

		return this.replace(/%s/g, function() {
			return args[i++];
		});
	};

	function formatNumberFixed(d, digits) {
		return d.toFixed(digits).replace(/\./, _['.'])
	}

	function formatNumber(d, digits) {
		digits--;

		for (var v = d; v >= 10 && digits > 0; v /= 10)
			digits--;

		// avoid toPrecision as it might produce strings in exponential notation
		return formatNumberFixed(d, digits);
	}

	function prettyPackets(d) {
		return _['%s packets/s'].sprintf(formatNumberFixed(d, 0));
	}

	function prettyPrefix(prefixes, step, d) {
		var prefix = 0;

		if (d === undefined)
			return "- ";

		while (d > step && prefix < prefixes.length - 1) {
			d /= step;
			prefix++;
		}

		d = formatNumber(d, 3);
		return d + " " + prefixes[prefix];
	}

	function prettySize(d) {
		return prettyPrefix([ "", "K", "M", "G", "T" ], 1024, d);
	}

	function prettyBits(d) {
		return prettySize(8 * d) + "bps";
	}

	function prettyBytes(d) {
		return prettySize(d) + "B";
	}

	var formats = {
		'id': function(value) {
			return value;
		},
		'decimal': function(value) {
			return formatNumberFixed(value, 2);
		},
		'percent': function(value) {
			return _['%s used'].sprintf(formatNumber(100 * value, 3) + '%');
		},
		'memory': function(memory) {
			var usage = 1 - memory.available / memory.total
			return formats.percent(usage);
		},
		'time': function(seconds) {
			var minutes = Math.round(seconds / 60);

			var days = Math.floor(minutes / 1440);
			var hours = Math.floor((minutes % 1440) / 60);
			minutes = Math.floor(minutes % 60);

			var out = '';

			if (days === 1)
				out += _['1 day'] + ', ';
			else if (days > 1)
				out += _['%s days'].sprintf(days) + ", ";

			out += hours + ":";

			if (minutes < 10)
				out += "0";

			out += minutes;

			return out;
		},
		'packetsDiff': function(packets, packetsPrev, diff) {
			if (diff > 0)
				return prettyPackets((packets-packetsPrev) / diff);

		},
		'bytesDiff': function(bytes, bytesPrev, diff) {
			if (diff > 0)
				return prettyBits((bytes-bytesPrev) / diff);
		},
		'bytes': function(bytes) {
			return prettyBytes(bytes);
		},
		'neighbour': function(addr) {
			if (!addr)
				return '';

			for (var i in interfaces) {
				var iface = interfaces[i];
				var neigh = iface.lookup_neigh(addr);
				if (!neigh)
					continue;
				return 'via ' + neigh.get_hostname() + ' (' + i + ')';
			}

			return 'via ' + addr + ' (unknown iface)';
		}
	}


	function resolve_key(obj, key) {
		key.split('/').forEach(function(part) {
			if (obj)
				obj = obj[part];
		});

		return obj;
	}

	function add_event_source(url, handler) {
		var source = new EventSource(url);
		var prev = {};
		source.onmessage = function(m) {
			var data = JSON.parse(m.data);
			handler(data, prev);
			prev = data;
		}
		source.onerror = function() {
			source.close();
			window.setTimeout(function() {
				add_event_source(url, handler);
			}, 3000);
		}
	}

	var node_address = document.body.getAttribute('data-node-address');

	var location;
	try {
		location = JSON.parse(document.body.getAttribute('data-node-location'));
	} catch (e) {
	}


	function update_mesh_vpn(data) {
		function add_group(peers, d) {
			Object.keys(d.peers || {}).forEach(function(peer) {
				peers.push([peer, d.peers[peer]]);
			});

			Object.keys(d.groups || {}).forEach(function(group) {
				add_group(peers, d.groups[group]);
			});

			return peers;
		}

		var div = document.getElementById('mesh-vpn');
		if (!data) {
			div.style.display = 'none';
			return;
		}

		div.style.display = '';
		var table = document.getElementById('mesh-vpn-peers');
		while (table.lastChild)
			table.removeChild(table.lastChild);

		var peers = add_group([], data);
		peers.sort();

		peers.forEach(function (peer) {
			var tr = document.createElement('tr');

			var th = document.createElement('th');
			th.textContent = peer[0];
			tr.appendChild(th);

			var td = document.createElement('td');
			if (peer[1])
				td.textContent = _['connected'] + ' (' + formats.time(peer[1].established) + ')';
			else
				td.textContent = _['not connected'];
			tr.appendChild(td);

			table.appendChild(tr);
		});
	}

	function update_radios(wireless) {
		function channel(frequency) {
			if (frequency===2484)
				return 14

			if (2412<=frequency && frequency<=2472)
				return (frequency-2407)/5

			if (5160<=frequency && frequency<=5885)
				return (frequency-5000)/5

			return 'unknown'
		}

		var div = document.getElementById('radios');
		if (!wireless) {
			div.style.display = 'none';
			return;
		}
		div.style.display = '';

		var table = document.getElementById('radio-devices');
		while (table.lastChild)
			table.removeChild(table.lastChild);

		wireless.sort(function (a, b) {
			return a.phy - b.phy;
		});

		wireless.forEach(function (radio) {
			var tr = document.createElement('tr');

			var th = document.createElement('th');
			th.textContent = "phy" + radio.phy;
			tr.appendChild(th);

			var td = document.createElement('td');
			td.innerHTML = radio.frequency + " MHz<br />Channel " + channel(radio.frequency);
			tr.appendChild(td);

			table.appendChild(tr);
		});
	}

	var statisticsElems = document.querySelectorAll('[data-statistics]');

	add_event_source('/cgi-bin/dyn/statistics', function(data, dataPrev) {
		var diff = data.uptime - dataPrev.uptime;

		statisticsElems.forEach(function(elem) {
			var stat = elem.getAttribute('data-statistics');
			var format = elem.getAttribute('data-format');

			var valuePrev = resolve_key(dataPrev, stat);
			var value = resolve_key(data, stat);
			try {
				var text = formats[format](value, valuePrev, diff);
				if (text !== undefined)
					elem.textContent = text;
			} catch (e) {
				console.error(e);
			}
		});

		try {
			update_mesh_vpn(data.mesh_vpn);
		} catch (e) {
			console.error(e);
		}
		try {
			update_radios(data.wireless);
		} catch (e) {
			console.error(e);
		}
	})

	function haversine(lat1, lon1, lat2, lon2) {
		var rad = Math.PI / 180;
		lat1 *= rad; lon1 *= rad; lat2 *= rad; lon2 *= rad;

		var R = 6372.8; // km
		var dLat = lat2 - lat1;
		var dLon = lon2 - lon1;
		var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
		var c = 2 * Math.asin(Math.sqrt(a));
		return R * c;
	}

	var interfaces = {};

	function Signal(color) {
		var canvas = document.createElement('canvas');
		var ctx = canvas.getContext('2d');
		var value = null;
		var radius = 1.2;

		function drawPixel(x, y) {
			ctx.beginPath();
			ctx.fillStyle = color;
			ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
			ctx.closePath();
			ctx.fill();
		}

		return {
			'canvas': canvas,
			'highlight': false,

			'resize': function(w, h) {
				var lastImage;
				try {
					ctx.getImageData(0, 0, w, h);
				} catch (e) {}
				canvas.width = w;
				canvas.height = h;
				if (lastImage)
					ctx.putImageData(lastImage, 0, 0);
			},

			'draw': function(x, scale) {
				var y = scale(value);

				ctx.clearRect(x, 0, 5, canvas.height)

				if (y)
					drawPixel(x, y)
			},

			'set': function (d) {
				value = d;
			},
		};
	}

	function SignalGraph() {
		var min = -100, max = 0;
		var i = 0;

		var signals = [];

		var canvas = document.createElement('canvas');
		canvas.className = 'signalgraph';
		canvas.height = 200;

		var ctx = canvas.getContext('2d');

		function scaleInverse(n, min, max, height) {
			return (min * n + max * (height - n)) / height;
		}

		function scale(n, min, max, height) {
			return (1 - (n - min) / (max - min)) * height;
		}

		function drawGrid() {
			var nLines = Math.floor(canvas.height / 40);
			ctx.save();
			ctx.lineWidth = 0.5;
			ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)';
			ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
			ctx.textAlign = 'end';
			ctx.textBaseline = 'bottom';

			ctx.beginPath();

			for (var i = 0; i < nLines; i++) {
				var y = canvas.height - i * 40;
				ctx.moveTo(0, y - 0.5);
				ctx.lineTo(canvas.width, y - 0.5);
				var dBm = Math.round(scaleInverse(y, min, max, canvas.height)) + ' dBm';

				ctx.save();
				ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
				ctx.lineWidth = 4;
				ctx.miterLimit = 2;
				ctx.strokeText(dBm, canvas.width - 5, y - 2.5);
				ctx.fillText(dBm, canvas.width - 5, y - 2.5);
				ctx.restore();
			}

			ctx.stroke();

			ctx.strokeStyle = 'rgba(0, 0, 0, 0.83)';
			ctx.lineWidth = 1.5;
			ctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);

			ctx.restore();
		}

		function resize() {
			canvas.width = canvas.clientWidth;
			signals.forEach(function(signal) {
				signal.resize(canvas.width, canvas.height);
			});
		}
		resize();

		function draw() {
			if (canvas.clientWidth === 0)
				return;

			if (canvas.width !== canvas.clientWidth)
				resize();

			ctx.clearRect(0, 0, canvas.width, canvas.height);

			var highlight = false;
			signals.forEach(function(signal) {
				if (signal.highlight)
					highlight = true;
			});

			ctx.save();
			signals.forEach(function(signal) {
				if (highlight)
					ctx.globalAlpha = 0.2;

				if (signal.highlight)
					ctx.globalAlpha = 1;

				signal.draw(i, function(value) {
					return scale(value, min, max, canvas.height);
				});
				ctx.drawImage(signal.canvas, 0, 0);
			});
			ctx.restore();

			ctx.save();
			ctx.beginPath();
			ctx.strokeStyle = 'rgba(255, 180, 0, 0.15)';
			ctx.lineWidth = 5;
			ctx.moveTo(i + 2.5, 0);
			ctx.lineTo(i + 2.5, canvas.height);
			ctx.stroke();

			drawGrid();
		}

		window.addEventListener('resize', draw);

		var last = 0;
		function step(timestamp) {
			var delta = timestamp - last;

			if (delta > 40) {
				draw();
				i = (i + 1) % canvas.width;
				last = timestamp;
			};

			window.requestAnimationFrame(step);
		}

		window.requestAnimationFrame(step);

		return {
			'el': canvas,

			'addSignal': function(signal) {
				signals.push(signal);
				signal.resize(canvas.width, canvas.height);
			},

			'removeSignal': function(signal) {
				signals.splice(signals.indexOf(signal), 1);
			},
		};
	}

	function Neighbour(iface, addr, color, destroy) {
		var th = iface.table.firstElementChild;
		var el = iface.table.insertRow();

		var tdHostname = el.insertCell();
		tdHostname.setAttribute('data-label', th.children[0].textContent);

		if (iface.wireless) {
			var marker = document.createElement("span");
			marker.textContent = "⬤ ";
			marker.style.color = color;
			tdHostname.appendChild(marker);
		}

		var hostname = document.createElement("span");
		hostname.textContent = addr;
		tdHostname.appendChild(hostname);

		var meshAttrs = {};

		function add_attr(attr) {
			var key = attr.getAttribute('data-key');
			if (!key)
				return;

			var suffix = attr.getAttribute('data-suffix') || '';

			var td = el.insertCell();
			td.textContent = '-';
			td.setAttribute('data-label', attr.textContent);

			meshAttrs[key] = {
				'td': td,
				'suffix': suffix,
			};
		}

		for (var i = 0; i < th.children.length; i++)
			add_attr(th.children[i]);

		var tdSignal;
		var tdDistance;
		var tdInactive;
		var signal;

		if (iface.wireless) {
			tdSignal = el.insertCell();
			tdSignal.textContent = '-';
			tdSignal.setAttribute(
				'data-label',
				th.children[Object.keys(meshAttrs).length + 1].textContent
			);

			tdDistance = el.insertCell();
			tdDistance.textContent = '-';
			tdDistance.setAttribute(
				'data-label',
				th.children[Object.keys(meshAttrs).length + 2].textContent
			);

			tdInactive = el.insertCell();
			tdInactive.textContent = '-';
			tdInactive.setAttribute(
				'data-label',
				th.children[Object.keys(meshAttrs).length + 3].textContent
			);

			signal = Signal(color);
			iface.signalgraph.addSignal(signal);
		}

		el.onmouseenter = function () {
			el.classList.add("highlight");
			if (signal)
				signal.highlight = true;
		}

		el.onmouseleave = function () {
			el.classList.remove("highlight")
			if (signal)
				signal.highlight = false;
		}

		var timeout;

		function updated() {
			if (timeout)
				window.clearTimeout(timeout);

			timeout = window.setTimeout(function() {
				if (signal)
					iface.signalgraph.removeSignal(signal);

				el.parentNode.removeChild(el);
				destroy();
			}, 60000);
		}
		updated();

		function address_to_groups(addr) {
			if (addr.slice(0, 2) == '::')
				addr = '0' + addr;
			if (addr.slice(-2) == '::')
				addr = addr + '0';

			var parts = addr.split(':');
			var n = parts.length;
			var groups = [];

			parts.forEach(function(part, i) {
				if (part === '') {
					while (n++ <= 8)
						groups.push(0);
				} else {
					if (!/^[a-f0-9]{1,4}$/i.test(part))
						return;

					groups.push(parseInt(part, 16));
				}
			});

			return groups;
		}

		function address_to_binary(addr) {
			var groups = address_to_groups(addr);
			if (!groups)
				return;

			var ret = '';
			groups.forEach(function(group) {
				ret += ('0000000000000000' + group.toString(2)).slice(-16);
			});

			return ret;
		}

		function common_length(a, b) {
			var i;
			for (i = 0; i < a.length && i < b.length; i++) {
				if (a[i] !== b[i])
				break;
			}
			return i;
		}

		function choose_address(addresses) {
			var node_bin = address_to_binary(node_address);

			if (!addresses || !addresses[0])
				return;

			addresses = addresses.map(function(addr) {
				var addr_bin = address_to_binary(addr);
				if (!addr_bin)
					return [-1];

				var common_prefix = 0;
				if (node_bin)
					common_prefix = common_length(node_bin, addr_bin);

				return [common_prefix, addr_bin, addr];
			});

			addresses.sort(function(a, b) {
				if (a[0] < b[0])
					return 1;
				else if (a[0] > b[0])
					return -1;
				else if (a[1] < b[1])
					return -1;
				else if (a[1] > b[1])
					return 1;
				else
					return 0;

			});

			var address = addresses[0][2];
			if (address && !/^fe80:/i.test(address))
				return address;
		}

		return {
			'get_hostname': function() {
				return hostname.textContent;
			},
			'update_nodeinfo': function(nodeinfo) {
				var addr = choose_address(nodeinfo.network.addresses);
				if (addr) {
					if (hostname.nodeName.toLowerCase() === 'span') {
						var oldHostname = hostname;
						hostname = document.createElement('a');
						oldHostname.parentNode.replaceChild(hostname, oldHostname);
					}

					hostname.href = 'http://[' + addr + ']/';
				}

				hostname.textContent = nodeinfo.hostname;

				if (location && nodeinfo.location) {
					var distance = haversine(
						location.latitude, location.longitude,
						nodeinfo.location.latitude, nodeinfo.location.longitude
					);
					tdDistance.textContent = Math.round(distance * 1000) + " m"
				}

				updated();
			},
			'update_mesh': function(mesh) {
				Object.keys(meshAttrs).forEach(function(key) {
					var attr = meshAttrs[key];
					attr.td.textContent = mesh[key] + attr.suffix;
				});

				updated();
			},
			'update_wifi': function(wifi) {
				var inactiveLimit = 200;

				tdSignal.textContent = wifi.signal;
				tdInactive.textContent = Math.round(wifi.inactive / 1000) + ' s';

				el.classList.toggle('inactive', wifi.inactive > inactiveLimit);
				signal.set(wifi.inactive > inactiveLimit ? null : wifi.signal);

				updated();
			},
		};
	}

	function Interface(el, ifname, wireless) {

		var neighs = {};

		var signalgraph;
		if (wireless) {
			signalgraph = SignalGraph();
			el.appendChild(signalgraph.el);
		}

		var info = {
			'table': el.firstElementChild,
			'signalgraph': signalgraph,
			'ifname': ifname,
			'wireless': wireless,
		};

		var nodeinfo_running = false;
		var want_nodeinfo = {};

		var graphColors = [];
		function get_color() {
			if (!graphColors[0])
				graphColors = ["#396AB1", "#DA7C30", "#3E9651", "#CC2529", "#535154", "#6B4C9A", "#922428", "#948B3D"];

			return graphColors.shift();
		}

		function neigh_addresses(nodeinfo) {
			var addrs = [];

			var mesh = nodeinfo.network.mesh;
			Object.keys(mesh).forEach(function(meshif) {
				var ifaces = mesh[meshif].interfaces;
				Object.keys(ifaces).forEach(function(ifaceType) {
					ifaces[ifaceType].forEach(function(addr) {
						addrs.push(addr);
					});
				});
			});

			return addrs;
		}

		function load_nodeinfo() {
			if (nodeinfo_running)
				return;

			nodeinfo_running = true;

			var source = new EventSource('/cgi-bin/dyn/neighbours-nodeinfo?' + encodeURIComponent(ifname));
			source.addEventListener("neighbour", function(m) {
				try {
					var data = JSON.parse(m.data);
					neigh_addresses(data).forEach(function(addr) {
						var neigh = neighs[addr];
						if (neigh) {
							delete want_nodeinfo[addr];
							try {
								neigh.update_nodeinfo(data);
							} catch (e) {
								console.error(e);
							}
						}
					});
				} catch (e) {
					console.error(e);
				}
			}, false);

			source.onerror = function() {
				source.close();
				nodeinfo_running = false;

				Object.keys(want_nodeinfo).forEach(function (addr) {
					if (want_nodeinfo[addr] > 0) {
						want_nodeinfo[addr]--;
						load_nodeinfo();
					}
				});
			}
		}

		function lookup_neigh(addr) {
			return neighs[addr];
		}

		function get_neigh(addr) {
			var neigh = neighs[addr];
			if (!neigh) {
				want_nodeinfo[addr] = 3;
				neigh = neighs[addr] = Neighbour(info, addr, get_color(), function() {
					delete want_nodeinfo[addr];
					delete neighs[addr];
				});
				load_nodeinfo();
			}

			return neigh;
		}

		if (wireless) {
			add_event_source('/cgi-bin/dyn/stations?' + encodeURIComponent(ifname), function(data) {
				Object.keys(data).forEach(function (addr) {
					var wifi = data[addr];

					get_neigh(addr).update_wifi(wifi);
				});
			});
		}

		return {
			'get_neigh': get_neigh,
			'lookup_neigh': lookup_neigh
		};
	}

	document.querySelectorAll('[data-interface]').forEach(function(elem) {
		var ifname = elem.getAttribute('data-interface');
		var address = elem.getAttribute('data-interface-address');
		var wireless = !!elem.getAttribute('data-interface-wireless');

		interfaces[ifname] = Interface(elem, ifname, wireless);
	});

	var mesh_provider = document.body.getAttribute('data-mesh-provider');
	if (mesh_provider) {
		add_event_source(mesh_provider, function(data) {
			Object.keys(data).forEach(function (addr) {
				var mesh = data[addr];
				var iface = interfaces[mesh.ifname];
				if (!iface)
					return;

				iface.get_neigh(addr).update_mesh(mesh);
			});
		});
	}
})();