var g_following_cache = {};
var g_following_deep_cache = {};
var g_about_cache = {};

async function following(db, id) {
	if (g_following_cache[id]) {
		return g_following_cache[id];
	}
	var o = await db.get(id + ":following");
	const k_version = 5;
	var f = o ? JSON.parse(o) : o;
	if (!f || f.version != k_version) {
		f = {users: [], sequence: 0, version: k_version};
	}
	f.users = new Set(f.users);
	await ssb.sqlAsync(
		"SELECT "+
		"  sequence, "+
		"  json_extract(content, '$.contact') AS contact, "+
		"  json_extract(content, '$.following') AS following "+
		"FROM messages "+
		"WHERE "+
		"  author = ?1 AND "+
		"  sequence > ?2 AND "+
		"  json_extract(content, '$.type') = 'contact' "+
		"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
		"ORDER BY sequence",
		[id, f.sequence],
		function(row) {
			if (row.following) {
				f.users.add(row.contact);
			} else {
				f.users.delete(row.contact);
			}
			f.sequence = row.sequence;
		});
	var as_set = f.users;
	f.users = Array.from(f.users).sort();
	var j = JSON.stringify(f);
	if (o != j) {
		await db.set(id + ":following", j);
	}
	f.users = as_set;
	g_following_cache[id] = f.users;
	return f.users;
}

async function followingDeep(db, seed_ids, depth) {
	if (depth <= 0) {
		return seed_ids;
	}
	var key = JSON.stringify([seed_ids, depth]);
	if (g_following_deep_cache[key]) {
		return g_following_deep_cache[key];
	}
	var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
	var ids = [].concat(...f);
	var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
	x = [...new Set([].concat(...x, ...seed_ids))].sort();
	g_following_deep_cache[key] = x;
	return x;
}

async function getAbout(db, id) {
	if (g_about_cache[id]) {
		return g_about_cache[id];
	}
	var o = await db.get(id + ":about");
	const k_version = 4;
	var f = o ? JSON.parse(o) : o;
	if (!f || f.version != k_version) {
		f = {about: {}, sequence: 0, version: k_version};
	}
	await ssb.sqlAsync(
		"SELECT "+
		"  sequence, "+
		"  content "+
		"FROM messages "+
		"WHERE "+
		"  author = ?1 AND "+
		"  sequence > ?2 AND "+
		"  json_extract(content, '$.type') = 'about' AND "+
		"  json_extract(content, '$.about') = ?1 "+
		"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
		"ORDER BY sequence",
		[id, f.sequence],
		function(row) {
			f.sequence = row.sequence;
			if (row.content) {
				var about = {};
				try {
					about = JSON.parse(row.content);
				} catch {
				}
				delete about.about;
				delete about.type;
				f.about = Object.assign(f.about, about);
			}
		});
	var j = JSON.stringify(f);
	if (o != j) {
		await db.set(id + ":about", j);
	}
	g_about_cache[id] = f.about;
	return f.about;
}

async function getSize(db, id) {
	let size = 0;
	await ssb.sqlAsync(
		"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
		[id],
		function (row) {
			size += row.size;
		});
	return size;
}

function niceSize(bytes) {
	let value = bytes;
	let unit = 'B';
	const k_units = ['kB', 'MB', 'GB', 'TB'];
	for (let u of k_units) {
		if (value >= 1024) {
			value /= 1024;
			unit = u;
		} else {
			break;
		}
	}
	return Math.round(value * 10) / 10 + ' ' + unit;
}

async function buildTree(db, root, indent, depth) {
	var f = await following(db, root);
	var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
	if (depth > 0) {
		for (let next of f) {
			result += await buildTree(db, next, indent + '  ', depth - 1);
		}
	}
	return result;
}

async function main() {
	await app.setDocument('<pre style="color: #fff">building...</pre>');
	var db = await database('ssb');
	var whoami = await ssb.getIdentities();
	var tree = '';
	for (let id of whoami) {
		await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
		tree += await buildTree(db, id, '', 2);
	}
	await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
}

main();