import * as d3 from "d3";
// use jsx templating
import { h, Fragment } from 'start-dom-jsx';
import anime from 'animejs/lib/anime.es.js';

console.log(anime)

// const projection = d3.geoAitoff();

function degreesToRadians(degrees) {
	return (degrees * Math.PI) / 180;
}

function latLonToOffsets(latitude, longitude, mapWidth, mapHeight) {
	const FE = 180; // false easting
	const radius = mapWidth / (2 * Math.PI);

	const latRad = degreesToRadians(latitude);
	const lonRad = degreesToRadians(longitude + FE);

	const x = lonRad * radius;

	const yFromEquator = radius * Math.log(Math.tan(Math.PI / 4 + latRad / 2));
	const y = mapHeight / 2 - yFromEquator;

	return { x, y };
}

export function viz(vizEl) {
	console.log(d3)
	// vizEl.appendChild(<h2>Hello</h2>);

	fetch("data/parsed_requests.json")
		.then((response) => response.json())
		.then(start)
		.catch((error) => {
			console.error(error);
			// alert("Data could not be loaded.")
		});

	// d3.queue()
	// 	.defer(d3.json, "data/parsed_requests.json")  // World shape
	// 	.await(start)
}

function lerp(X, Y, t) {
	return X * t + Y * (1 - t)
}

function inverse_lerp(a, b, x) {
	return (x - a) / (b - a);
}

interface IndexObject {
	[index: string]: Array<number>;
}

/**
 * A scene
 */
class Scene {
	// setup: CallableFunction;
	animation: CallableFunction; // the function that returns an anime.js object

	constructor(animation: CallableFunction) {
		// this.setup = setup;
		this.animation = animation;
	}

	async run () {
		const anim = this.animation();
		console.log('anim', anim);
		await anim.finished
	}

}

class Sequence {
	scenes: Array<Scene>;

	constructor() {
		this.scenes = [];
	}

	async run() {
		while(true) {
			for(const scene of this.scenes){
				console.log('scene', scene);
				await scene.run();
				console.log('ran')
			}
		}
	}
}


function buildIndex(objectList: Array<Object>, field: string): IndexObject {
	const index = {}
	for (const idx in objectList) {
		const v = objectList[idx][field]
		if (!index.hasOwnProperty(v)) {
			index[v] = [idx]
		} else {
			index[v].push(idx)
		}
	}
	return index;
}



function start(parsed_requests) {
	const nodes = parsed_requests.nodes;
	const edges: Array<Object> = parsed_requests.edges;

	const svg = d3.select('svg');
	const g_libraries = svg.append("g").attr("id", "libraries");
	const g_movements = svg.append("g").attr("id", "movements");
	const width = +svg.attr("width"),
		height = +svg.attr("height");

	// let libraries = {}
	// console.log(nodes, edges)
	// filter nodes with only having both Latitude and Longitude.
	// then map these coordinates to the canvas space
	const valid_nodes = nodes.filter((n) => n.lat && n.lon).map((node) => {
		// const { x: x1, y: y1 } = latLonToOffsets(node.lat, node.lon, 100000, 100000)
		const { x: x1, y: y1 } = latLonToOffsets(node.lat, node.lon, 100000, 100000)
		console.log(x1, y1)
		const x2 = inverse_lerp(1358, 1380, x1);
		const y2 = inverse_lerp(32862, 32900, y1);

		const margin = 200;
		const x = x2 * (width - 2 * margin) + margin;
		const y = y2 * (height - 2 * margin) + margin;
		// console.log(x2, y2)
		// console.log(node)

		node['x'] = x;
		node['y'] = y;
		return node
	})

	// create an index to access the node objects by their name
	const nodeMap = Object.fromEntries(valid_nodes.map(d => [d['name'], d]));
	const edgeIndexBarcode = buildIndex(edges, 'Barcode');
	// console.log(edgeIndexBarcode);

	const links = edges.filter(l => nodeMap[l['Owning Library Name']] && nodeMap[l['Pickup Location']]).map((l, idx) => {
		l.source = nodeMap[l['Owning Library Name']];
		l.target = nodeMap[l['Pickup Location']];
		l.nr = idx;
		return l;
	});
	// .filter((link, index, self) =>
	// 	// remove incidental duplicates
	// 	index === self.findIndex((l) => (
	// 		l.source.id === link.source.id && l.target.id === link.target.id
	// 	))
	// );

	// Reformat the list of link. Note that columns in csv file are called long1, long2, lat1, lat2
	// var link = []
	// edges.forEach(function (row) {
	// 	source = [+row.long1, +row.lat1]
	// 	target = [+row.long2, +row.lat2]
	// 	topush = { type: "LineString", coordinates: [source, target] }
	// 	link.push(topush)
	// })

	// Add the path
	const libraries = g_libraries.selectAll(".library");
	libraries.data(valid_nodes, d => d.code)
		.join((enter) => {
			let group = enter.append("g")
				// .attr("class", getClasses)
				// .attr("id", (n) => getIdForTitle(n.fulltext));
				.attr("id", (n) => n.code)
				.attr("transform", (n) => `translate(${n.x}, ${n.y})`);

			// group.on("click", (evt, n) => {
			// 	evt.stopPropagation(); this.selectNode(n);
			// });
			// group.on("mouseover", (evt, n) => {
			// 	this.hoverNode(evt, n);
			// });
			// group.on("mouseout", (evt, n) => {
			// 	this.endHoverNode(n);
			// });
			group.append('circle').attr("r", 5 /*this.nodeSize*/);
			group.append('circle').attr("r", 20 /*this.nodeSize*/).style("fill", "none").style("stroke", "white").style("stroke-width", "10");
			// group.append('path')
			// 	.attr('d', (n) => {
			// 		return getSymbolForNode(n)(n);
			// 	})
			var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "4").attr('x', 35).text((n) => n.name);
			return group
		})

	const movements_a = g_movements.selectAll(".movement");
	const movements = movements_a
		.data(links)
		.join(
			enter => {
				let group = enter.append("g")
					.attr("class", (l) => "link " /* TODO type of movement */)
					.attr("id", (l) => `link${l.nr}`);
				group.append("path")
					// .attr("marker-end", "url(#arrowHead)")
					.attr('id', (d, i) => 'linkpath_' + i)
					// .on("mouseover", (ev, link) => {
					// 	d3.select(ev.target).classed('hover', true);
					// 	const nodes = document.getElementsByClassName('node');
					// 	for (let n of nodes) {
					// 		const d = d3.select(n).datum();
					// 		if (d == link.target || d == link.source) {
					// 			n.classList.add('linkHover');
					// 		}
					// 	}

					// 	this.showRelationTooltip(link, ev);
					// }).on("mouseout", (ev, link) => {
					// 	this.hideTooltip();
					// 	d3.select(ev.target).classed('hover', false);
					// 	const nodes = document.getElementsByClassName('linkHover');
					// 	while (nodes.length) {
					// 		nodes[0].classList.remove('linkHover');
					// 	}
					// }).on("click", (ev, link) => {
					// 	ev.stopPropagation();
					// 	this.selectNode(link.source);
					// })
					;
				// group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) {
				// 	return l.name;
				// });
				// group.append("text")
				//         .attr("class", "labelText")
				//         .attr("dx", 20)
				//         .attr("dy", 0)
				//         .style("fill", "red")
				//     .append("textPath")
				//         .attr("xlink:href", function (d, i) { return "#linkid_" + i; })
				//         .attr("startOffset","50%")
				//         .text((d,i) => d.name );
				return group;
			}
		)
		;

	movements.each(function (l) {
		let sourceX, targetX, midX, dx, dy, angle;

		// This mess makes the arrows exactly perfect.
		// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
		if (l.source.x < l.target.x) {
			sourceX = l.source.x;
			targetX = l.target.x;
		} else if (l.target.x < l.source.x) {
			targetX = l.target.x;
			sourceX = l.source.x;
		} else if (l.target.isCircle) {
			targetX = sourceX = l.target.x;
		} else if (l.source.isCircle) {
			targetX = sourceX = l.source.x;
		} else {
			midX = (l.source.x + l.target.x) / 2;
			if (midX > l.target.x) {
				midX = l.target.x;
			} else if (midX > l.source.x) {
				midX = l.source.x;
			} else if (midX < l.target.x) {
				midX = l.target.x;
			} else if (midX < l.source.x) {
				midX = l.source.x;
			}
			targetX = sourceX = midX;
		}

		dx = targetX - sourceX;
		dy = l.target.y - l.source.y;
		angle = Math.atan2(dx, dy);

		var srcSize = 5; //_mapGraph.getSizeForNode(l.source);
		var tgtSize = 5; //_mapGraph.getSizeForNode(l.target);

		// Compute the line endpoint such that the arrow
		// it not in the center, but rather slightly out of it
		// use a small ofset for the angle to compensate roughly for the curve 
		l.sourceX = sourceX + Math.sin(angle + .5) * srcSize;
		l.targetX = targetX - Math.sin(angle - .5) * tgtSize;
		l.sourceY = l.source.y + Math.cos(angle + .5) * srcSize;
		l.targetY = l.target.y - Math.cos(angle - .5) * tgtSize;


		// find radius of arc based on distance between points
		// add a jitter to spread out the lines when links are stacked
		const dr = Math.sqrt(dx * dx + dy * dy) * (.7 + Math.random() * .6);

		// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
		let rel = d3.select(this);
		rel.select("path")
			.attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1   ${l.targetX},${l.targetY}`)


	})

	// const edgesToHighlight = edgeIndexBarcode["0139900930"].map((i) => edges[i])
	// const edgesToHighlight = edgeIndexBarcode["0182126011"].map((i) => edges[i])
	// const edgesToHighlight = edgeIndexBarcode["0139204135"].map((i) => edges[i])
	let edgesToHighlight: Array<Object> = [];
	for (const code in edgeIndexBarcode) {
		if (edgeIndexBarcode[code].length < 4) {
			continue;
		}

		for (const i of edgeIndexBarcode[code]) {
			// console.log(i)
			edgesToHighlight.push(edges[i]);
		}
	}

	// const edgeToSelect = edges.filter(e => e.nr == 530)[0]
	const edgeToSelect = edges.filter(e => e.Barcode == "0133040931")[0]
	edgesToHighlight = edges.filter(e => e.Barcode == edgeToSelect.Barcode)
	// edgeToSelect.
	// console.log(530, edges[530].nr, edges[530])
	// edgesToHighlight = [];
	// edgesToHighlight.push(edges[530])
	highlightTrajectory(edgesToHighlight);
	// console.log(libraries)

	function scene_01_FollowItem() {
		const edgeToSelect = edges.filter(e => e.Barcode == "0133040931")[0]
		edgesToHighlight = edges.filter(e => e.Barcode == edgeToSelect.Barcode)

		highlightTrajectory(edgesToHighlight);
		
		const tl = anime.timeline({})
		// console.log(tl)
		for(const edge of edgesToHighlight) {
			// document.getElementById(`#linkpath_${edge.nr}`).dataset.length = document.getElementById(`#linkpath_${edge.nr}`).getTotalLength
			tl.add({
				targets: `#linkpath_${edge.nr}`,
				stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'],
				// strokeDashoffset: [anime.setDashoffset, 0],
				duration: 500,
				  // easing: 'easeOutElastic(1, .8)',
				// direction: 'alternate'
			})
			.add({
				targets: `#linkpath_${edge.nr}`,
				// stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'],
				strokeDashoffset: [anime.setDashoffset, 0],
				easing: 'easeInOutSine',
				duration: 8000,
				  // easing: 'easeOutElastic(1, .8)',
				// direction: 'alternate'
			})
		}

		
		tl.add({
			targets: edgesToHighlight.map(edge => `#linkpath_${edge.nr}`),
			stroke: ['rgb(0, 255, 255)', 'rgb(255, 0, 0)'],
			duration: 3000
		})
		return tl;
		
	
	}

	const sequence = new Sequence();
	sequence.scenes.push(new Scene(scene_01_FollowItem))
	console.log(sequence)
	sequence.run()

	// document.getElementById('linkpath_3903').style.stroke = 'orange';
	// sequence.scenes.push(new Scene(() => {
	// 	const tl = anime.timeline({})
	// 	tl.add({
	// 		targets: '#linkpath_532',
	// 		stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'],
	// 		duration: 2000,
  	// 		// easing: 'easeOutElastic(1, .8)',
	// 		direction: 'alternate'
	// 	})
	// 	tl.add({
	// 		targets: '#linkpath_532',
	// 		// stroke: ['rgb(255, 255, 0)', 'rgb(255, 0, 255)'],
	// 		translateX: [0, -100, 0],
	// 		duration: 4000,
  	// 		easing: 'easeOutElastic(1, .8)',
	// 		// direction: 'alternate'
	// 	})
	// 	return tl
	// }));
	// sequence.scenes.push(new Scene(() => {
	// 	return anime({
	// 		targets: '#linkpath_530',
	// 		translateX: [0, 500],
	// 		duration: 2000,
	// 		direction: 'alternate'
	// 	})
	// }));
	// sequence.run()
}


// TODO: this is currently by barcode, but this should be the object itself.
function highlightTrajectory(edges: Array<Object>) {
	// console.log(edges)
	for (const edge of edges) {
		console.log(`#linkpath_${edge.nr}`)
		const selection = d3.select(`#linkpath_${edge.nr}`);
		// console.log(selection);
		// selection.style("stroke", "red");
		// selection.style("stroke-width", "5");
		selection.classed("selected", true);
	}
}