:: FamilyTreeJS [script]
'use strict';

var lastActiveSlave, lastSlaves, lastPC;

/*
	To use, add something like:

<div id="familyTree"></div>
	<span id="familyTreeLink">
		<<link "Pull up the file on her family tree.">>
		<<replace #familyTreeLink>>
			<<run renderFamilyTree($slaves, $activeSlave.ID)>>
		<</replace>>
	<</link>>
</span>

*/

window.renderFamilyTree = function(slaves, filterID) {

	var ftreeWidth,ftreeHeight;
	var chartWidth, chartHeight;
	var margin;
	d3.select('#ftree-canvas').remove();
	var svg = d3.select('#familyTree')
		.append('svg')
		.attr('id','ftree-canvas');
	var chartLayer = svg.append('g').classed('chartLayer', true);

	var range = 100;
	var data = buildFamilyTree(slaves, filterID);

	initFtreeSVG(data);
	runFtreeSim(data);

	function initFtreeSVG(data) {
		ftreeWidth = data.nodes.length * 45;
		ftreeHeight = data.nodes.length * 35;
		if(ftreeWidth < 600) {
			ftreeWidth = 600;
		} else if(ftreeWidth > 1920) {
			ftreeWidth = 1920;
		}
		if(ftreeHeight < 480) {
			ftreeHeight = 480;
		} else if(ftreeHeight > 1200) {
			ftreeHeight = 1200;
		}

		margin = {top:0, left:0, bottom:0, right:0 }


		chartWidth = ftreeWidth - (margin.left+margin.right)
		chartHeight = ftreeHeight - (margin.top+margin.bottom)

		svg.attr('width', ftreeWidth).attr('height', ftreeHeight)

		var defs = svg.append('defs');

		svg.append('defs').append('marker')
			.attr('id','arrowhead')
			.attr('viewBox','-0 -5 10 10')
			.attr('refX',13)
			.attr('refY',0)
			.attr('orient','auto')
			.attr('markerWidth',13)
			.attr('markerHeight',13)
			.attr('xoverflow','visible')
			.append('svg:path')
			.attr('d', 'M 0,-1 L 5,0 L 0,1')
			.attr('fill', '#a1a1a1')
			.style('stroke','none');

		chartLayer
			.attr('width', chartWidth)
			.attr('height', chartHeight)
			.attr('transform', 'translate('+[margin.left, margin.top]+')')
	}

	function runFtreeSim(data) {
		var simulation = d3.forceSimulation()
			.force('link', d3.forceLink().id(function(d) { return d.index }))
			.force('collide',d3.forceCollide( function(d){ return 60; }).iterations(4) )
			.force('charge', d3.forceManyBody().strength(-200).distanceMin(100).distanceMax(1000))
			.force('center', d3.forceCenter(chartWidth / 2, chartHeight / 2))
			.force('y', d3.forceY(100))
			.force('x', d3.forceX(200))

		var link = svg.append('g')
			.attr('class', 'link')
			.selectAll('link')
			.data(data.links)
			.enter()
			.append('line')
			.attr('marker-end','url(#arrowhead)')
			.attr('stroke', function(d) {
				if(d.type == 'homologous') {
					return '#862d59';
				} else if(d.type == 'paternal') {
					return '#24478f';
				} else {
					return '#aa909b';
				}
			})
			.attr('stroke-width', 2)
			.attr('fill', 'none');

		var node = svg.selectAll('.node')
			.data(data.nodes)
			.enter().append('g')
			.attr('class', 'node')
			.call(d3.drag()
			.on('start', dragstarted)
			.on('drag', dragged)
			.on('end', dragended));

		node.append('circle')
			.attr('r', function(d){ return d.r })
			.attr('stroke', function(d) {
				if(d.ID == filterID) {
					return '#ffff20';
				} else {
					return '#5a5a5a';
				}
			})
			.attr('class', 'node-circle')
			.attr('r', 20);

		node.append('text')
			.text(function(d) {
				var ssym;
				if(d.ID == -1) {
					if(d.dick == 1 && d.vagina == 1) {
						ssym = '☿'
					} else if (d.dick == 1) {
						ssym = '♂';
					} else if (d.vagina == 1) {
						ssym = '♀';
					}
				} else if (d.dick > 0 && d.vagina > -1) {
					ssym = '☿'
				} else if (d.dick > 0) {
					ssym = '♂';
				} else if (d.vagina > -1) {
					ssym = '♀';
				} else {
					ssym = '?';
				}
				return d.name + '('+ssym+')';
			})
			.attr('dy', 4)
			.attr('dx', function(d) { return -(8*d.name.length)/2; })
			.attr('class', 'node-text')
			.style('fill', function(d) {
				if(d.is_mother && d.is_father) {
					return '#b84dff';
				} else if(d.is_father) {
					return '#00ffff';
				} else if(d.is_mother) {
					return '#ff3399';
				} else if(d.unborn) {
					return '#a3a3c2';
				} else {
					return '#66cc66';
				}
			});

		var circles = svg.selectAll('.node-circle');
		var texts = svg.selectAll('.node-text');

		var ticked = function() {
			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; });

			node
				.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
		}

		simulation.nodes(data.nodes)
			.on('tick', ticked);

		simulation.force('link')
			.links(data.links);

		function dragstarted(d) {
			if (!d3.event.active) simulation.alphaTarget(0.3).restart();
			d.fx = d.x;
			d.fy = d.y;
		}

		function dragged(d) {
			d.fx = d3.event.x;
			d.fy = d3.event.y;
		}

		function dragended(d) {
			if (!d3.event.active) simulation.alphaTarget(0);
			d.fx = null;
			d.fy = null;
		}

	}
};

window.buildFamilyTree = function(slaves = State.variables.slaves, filterID) {
	var family_graph = {
		nodes: [],
		links: []
	};
	var node_lookup = {};
	var preset_lookup = {
		'-2': 'Social elite',
		'-3': 'Client',
		'-4': 'Former master',
		'-5': 'An arcology owner',
		'-6': 'A citizen'
	};
	var outdads = {};
	var outmoms = {};
	var kids = {};

	var fake_pc = {
		slaveName: State.variables.PC.name + '(You)',
		mother: State.variables.PC.mother,
		father: State.variables.PC.father,
		dick: State.variables.PC.dick,
		vagina: State.variables.PC.vagina,
		ID: State.variables.PC.ID
	};
	var charList = [fake_pc];
	charList.push.apply(charList, slaves);
	charList.push.apply(charList, State.variables.tanks);

	var unborn = {};
	for(var i = 0; i < State.variables.tanks.length; i++) {
		unborn[State.variables.tanks[i].ID] = true;
	}
	for (var i = 0; i < State.variables.cribs.length; i++) {
		unborn[State.variables.cribs[i].ID] = true;
	}

	for(var i = 0; i < charList.length; i++) {
		var mom = charList[i].mother;
		var dad = charList[i].father;

		if(mom) {
			if(!kids[mom]) {
				kids[mom] = {};
			}
			kids[mom].mother = true;
		}
		if(dad) {
			if(!kids[dad]) {
				kids[dad] = {};
			}
			kids[dad].father = true;
		}
	}

	for(var i = 0; i < charList.length; i++) {
		var character = charList[i];
		if(character.mother == 0 && character.father == 0 && !kids[character.ID]) {
			continue;
		}
		var mom = character.mother;
		if(mom < -6) {
			if (mom in State.variables.missingTable && State.variables.showMissingSlaves) {
				node_lookup[mom] = family_graph.nodes.length;
				var missing = State.variables.missingTable[mom];
				charList.push({ID: mom, mother: 0, father: 0, is_mother: true, dick: missing.dick, vagina: missing.vagina, slaveName: missing.slaveName});
			} else {
				if(typeof outmoms[mom] == 'undefined') {
					outmoms[mom] = [];
				}
				outmoms[mom].push(character.slaveName);
			}
		} else if(mom < 0 && typeof node_lookup[mom] == 'undefined' && typeof preset_lookup[mom] != 'undefined') {
			node_lookup[mom] = family_graph.nodes.length;
			charList.push({ID: mom, mother: 0, father: 0, is_father: true, dick: 0, vagina: 1, slaveName: preset_lookup[mom]});
		}

		var dad = character.father;
		if(dad < -6) {
			if (dad in State.variables.missingTable && State.variables.showMissingSlaves) {
				node_lookup[dad] = family_graph.nodes.length;
				var missing = State.variables.missingTable[dad];
				charList.push({ID: dad, mother: 0, father: 0, is_father: true, dick: missing.dick, vagina: missing.vagina, slaveName: missing.slaveName});
			} else {
				if(typeof outdads[dad] == 'undefined') {
					outdads[dad] = [];
				}
				outdads[dad].push(character.slaveName);
			}
		} else if(dad < 0 && typeof node_lookup[dad] == 'undefined' && typeof preset_lookup[dad] != 'undefined') {
			node_lookup[dad] = family_graph.nodes.length;
			charList.push({ID: dad, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: preset_lookup[dad]});
		}
	}
	var mkeys = Object.keys(outmoms);
	for(var i = 0; i < mkeys.length; i++) {
		var name;
		var key = mkeys[i];
		var names = outmoms[key];
		if(names.length == 1) {
			name = names[0];
		} else if(names.length == 2) {
			name = names.join(' and ');
		} else {
			names[-1] = 'and '+names[-1];
			name = names.join(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		//Outside extant slaves set
		charList.push({ID: key, mother: 0, father: 0, is_mother: true, dick: 0, vagina: 1, slaveName: name+"'s mother"});
	}

	var dkeys = Object.keys(outdads);
	for(var i = 0; i < dkeys.length; i++) {
		var name;
		var key = dkeys[i];
		var names = outdads[key];
		if(names.length == 1) {
			name = names[0];
		} else if(names.length == 2) {
			name = names.join(' and ');
		} else {
			names[-1] = 'and '+names[-1];
			name = names.join(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		//Outside extant slaves set
		charList.push({ID: key, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: name+"'s father"});
	}

	var charHash = {};
	for(var i = 0; i < charList.length; i++) {
		charHash[charList[i].ID] = charList[i];
	}

	var related = {};
	var seen = {};
	var saveTree = {};
	function relatedTo(character, targetID, relIDs = {tree: {}, related: false}) {
		relIDs.tree[character.ID] = true;
		if(related[character.ID]) {
			relIDs.related = true;
			return relIDs;
		}
		if(character.ID == targetID) {
			relIDs.related = true;
		}
		if(seen[character.ID]) {
			return relIDs;
		}
		seen[character.ID] = true;
		if(character.mother != 0) {
			if(charHash[character.mother]) {
				relatedTo(charHash[character.mother], targetID, relIDs);
			}
		}
		if(character.father != 0) {
			if(charHash[character.father]) {
				relatedTo(charHash[character.father], targetID, relIDs);
			}
		}
		return relIDs;
	}
	if(filterID) {
		if(charHash[filterID]) {
			var relIDs = relatedTo(charHash[filterID], filterID);
			for(var k in relIDs.tree) {
				related[k] = true;
			}
			for(var i = 0; i < charList.length; i++) {
				if(charHash[charList[i].ID]) {
					var pRelIDs = relatedTo(charHash[charList[i].ID], filterID);
					if(pRelIDs.related) {
						for(var k in pRelIDs.tree) {
							related[k] = true;
							if(saveTree[k]) {
								for(var k2 in saveTree[k].tree) {
									related[k2] = true;
								}
							}
						}
					}
					saveTree[charList[i].ID] = pRelIDs;
				}
			}
		}
	}

	for(var i = 0; i < charList.length; i++) {
		var character = charList[i];
		var char_id = character.ID;
		if(character.mother == 0 && character.father == 0 && !kids[char_id]) {
			continue;
		}
		if(filterID && !related[char_id]) {
			continue;
		}
		node_lookup[char_id] = family_graph.nodes.length;
		var char_obj = {
			ID: char_id,
			name: character.slaveName,
			dick: character.dick,
			unborn: !!unborn[char_id],
			vagina: character.vagina
		};
		if(kids[char_id]) {
			char_obj.is_mother = !!kids[char_id].mother;
			char_obj.is_father = !!kids[char_id].father;
		}	else {
			char_obj.is_mother = false;
			char_obj.is_father = false;
		}
		family_graph.nodes.push(char_obj);
	}

	for(var i = 0; i < charList.length; i++) {
		var character = charList[i];
		var char_id = character.ID;
		if(character.mother == 0 && character.father == 0 && !kids[char_id]) {
			continue;
		}
		if(filterID && !related[char_id]) {
			if(related[character.mother]) {
				console.log('wtf, mom');
			}
			if(related[character.father]) {
				console.log('wtf, dad');
			}
			continue;
		}
		if(typeof node_lookup[character.mother] != 'undefined') {
			var ltype;
			if(character.mother == character.father) {
				ltype = 'homologous';
			} else {
				ltype = 'maternal';
			}
			family_graph.links.push({
				type: ltype,
				target: node_lookup[char_id]*1,
				source: node_lookup[character.mother]*1
			});
		}
		if(character.mother == character.father) {
			continue;
		}
		if(typeof node_lookup[character.father] != 'undefined') {
			family_graph.links.push({
				type: 'paternal',
				target: node_lookup[char_id]*1,
				source: node_lookup[character.father]*1
			});
		}
	}
	return family_graph;
};

/*Old version. To use, do something like: 
<div id="editFamily">
	<div id="graph"></div>
</div>

<<run updateFamilyTree($activeSlave, $slaves, $PC)>>
<script>updateFamilyTree()</script>
If you want to update the tree, just re-call the run line.

If there's no active slave, you can do:

<<run updateFamilyTree(null, $slaves, $PC)>>
*/

window.updateFamilyTree = function(activeSlave = lastActiveSlave, slaves = lastSlaves, PC = lastPC) {
	lastActiveSlave = activeSlave;
	lastSlaves = slaves;
	lastPC = PC;
	var treeDepth = 0;
	var numTreeNodes = 0;

	var graphElement = document.getElementById("graph");
	if(!graphElement)
		return;
	graphElement.innerHTML = "";

	/* The way this code works is that we start with the activeSlave then we call
		slaveInfo() recursively to work our way up the tree finding their parents.

	*/

	function getSlave(id, expectedGenes) {
		if(id == -1) {
			return {"slaveName":"YOU", "ID":id, "physicalAge":PC.physicalAge, "genes":PC.genes, father:PC.father, mother:PC.mother};
		}
		if(id == 0) {
			return {"slaveName":"-", "ID":id, "genes":expectedGenes};
		}
		if(id == activeSlave.ID) {
			return activeSlave;
		}
		for(var i = 0; i < slaves.length; ++i) {
			if(slaves[i].ID == id)
				return slaves[i];
			}
		return {"slaveName":"-", "ID":id, "genes":expectedGenes};
		}

	function slaveInfo(slave, activeSlaveId, recursionProtectSlaveId = {}) {
	numTreeNodes = 0;
	treeDepth = 0;
	if(recursionProtectSlaveId[slave.ID]) {
		console.log("Recursion protection");
		return slaveInfo_(slave, activeSlaveId);
	}
	recursionProtectSlaveId[slave.ID] = true;

	if(typeof slave.father == "undefined" || typeof slave.mother == "undefined")
		return slaveInfo_(slave, activeSlaveId);

	if(slave.father == -1 || slave.mother == -1) {
		return slaveInfo(getSlave(-1), activeSlaveId, recursionProtectSlaveId);
	}
	if(slave.father != 0) {
		return slaveInfo(getSlave(slave.father, "unknownXY"), activeSlaveId, recursionProtectSlaveId);
	}

	if(slave.mother != 0) {
		return slaveInfo(getSlave(slave.mother, "unknownXX"), activeSlaveId, recursionProtectSlaveId);
	}
	return slaveInfo_(slave, activeSlaveId);
	}
	function slaveInfo_(slave, activeSlaveId, slavesAdded={}, depth = 1) {
	numTreeNodes += 1;
	treeDepth = Math.max(treeDepth, depth);
	var shouldAddChildren = false;
	if(!slavesAdded[slave.ID]) {
		shouldAddChildren = true;
		slavesAdded[slave.ID] = true;
	}
	var data = {
		"name": slave.slaveName + (slave.physicalAge?("&nbsp;(" + slave.physicalAge + ")"):""),
		"class" : slave.genes,
		"textClass": (activeSlaveId == slave.ID)?"emphasis":"",
		"marriages": [],
	};

	var spouseToChild = {};

	function maybeAddSpouseToChild(child) {
		if(child.ID == slave.ID)
		return;
		if (child.father == slave.ID) {
		if(!spouseToChild[child.mother]) {
			spouseToChild[child.mother] = []
		}
		spouseToChild[child.mother].push(child);
		} else if (child.mother == slave.ID) {
		if(!spouseToChild[child.father]) {
			spouseToChild[child.father] = []
		}
		spouseToChild[child.father].push(child);
		}
	}

	if(activeSlave.ID != PC.ID)
		maybeAddSpouseToChild(activeSlave);
		maybeAddSpouseToChild(getSlave(-1));

	for(var i = 0; i < slaves.length; ++i) {
		var child = slaves[i];
		if(child.ID != activeSlave.ID)
		maybeAddSpouseToChild(child);
	}

	for(var key in spouseToChild) {
		if(spouseToChild.hasOwnProperty(key)) {
			var children = shouldAddChildren?spouseToChild[key]:[];
			var spouse = getSlave(key, (slaves.genes=="XX")?"unknownXY":(slaves.genes=="XY")?"unknownXX":"unknown");
			var spouseName;
			if (spouse.ID != slave.ID){
				spouseName = spouse.slaveName + (spouse.physicalAge?("&nbsp;(" + spouse.physicalAge + ")"):"")
			} else {
				spouseName = (spouse.ID==-1)?"(yourself)":"(themselves)";
			}
			var marriage = {
				"spouse": {"name": spouseName, "class": spouse.genes},
				"children": children.map(function(x) { return slaveInfo_(x, activeSlaveId, slavesAdded, depth+1)} ),
			};
			data.marriages.push(marriage);
			}
		}
	return data;
	}

	if(activeSlave == PC || activeSlave == null)
		activeSlave = getSlave(-1)
		const treeData = [slaveInfo(activeSlave, activeSlave.ID)];
		console.log("Family tree is", treeData, 'and has:', numTreeNodes);

		var parentWidth = document.getElementById('editFamily').offsetWidth;

		console.log(parentWidth, document.getElementById('passages').offsetWidth);
		if(!parentWidth)
			parentWidth = document.body.offsetWidth - 483;

			console.log(parentWidth, Math.min(200 + 40*numTreeNodes,parentWidth-200) + 200);

			dTree.init(treeData, {
			target: "#graph",
			debug: true,
			height: 50 + 50*treeDepth, /* very rough heuristics */
			width: Math.min(200 + 40*numTreeNodes,
					parentWidth-200) + 200,
			callbacks: {
				nodeClick: function(name, extra) {
			}
		}
	});

};
