// Toposort - Topological sorting for node.js
// Copyright (c) 2012 by Marcel Klehr <mklehr@gmx.net>
// MIT LICENSE
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

export function toposort(nodes: Array<string>, edges: Array<Array<string>>): Array<string> {
	let cursor = nodes.length;
	const sorted: Array<string> = new Array(cursor) as Array<string>;
	const visited: Record<number, boolean> = {};
	let i = cursor;
	// Better data structures make algorithm much faster.
	const outgoingEdges = makeOutgoingEdges(edges);
	const nodesHash = makeNodesHash(nodes);

	// check for unknown nodes
	edges.forEach(function (edge) {
		if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) {
			throw new Error('Unknown token. Make sure to provide all tokens used in aliases.');
		}
	});

	while (i--) {
		if (!visited[i]) {
			visit(nodes[i], i, new Set());
		}
	}

	return sorted;

	function visit(node: string, j: number, predecessors: Set<string>): void {
		if (predecessors.has(node)) {
			let nodeRep;
			try {
				nodeRep = ', token was: ' + JSON.stringify(node);
			} catch {
				nodeRep = '';
			}
			throw new Error('Cyclic dependency' + nodeRep);
		}

		if (!nodesHash.has(node)) {
			throw new Error('Found unknown token. Make sure to provided all involved tokens. Unknown token: ' + JSON.stringify(node));
		}

		if (visited[j]) {
			return;
		}
		visited[j] = true;

		const outgoing: Array<string> = Array.from(outgoingEdges.get(node) || new Set());

		// eslint-disable-next-line no-cond-assign
		if (j = outgoing.length) {
			predecessors.add(node);
			do {
				const child = outgoing[--j];
				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
				visit(child, nodesHash.get(child)!, predecessors);
			} while (j);
			predecessors.delete(node);
		}

		sorted[--cursor] = node;
	}
}

function makeOutgoingEdges(arr: Array<Array<string>>): Map<string, Set<string>> {
	const edges: Map<string, Set<string>> = new Map();
	for (let i = 0, len = arr.length; i < len; i++) {
		const edge = arr[i];
		if (!edges.has(edge[0])) {
			edges.set(edge[0], new Set());
		}
		if (!edges.has(edge[1])) {
			edges.set(edge[1], new Set());
		}
		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
		edges.get(edge[0])!.add(edge[1]);
	}
	return edges;
}

function makeNodesHash(arr: Array<string>): Map<string, number> {
	const res: Map<string, number> = new Map();
	for (let i = 0, len = arr.length; i < len; i++) {
		res.set(arr[i], i);
	}
	return res;
}
