/**
 * @fileoverview Helpers to debug for code path analysis.
 * @author Toru Nagashima
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const debug = require("debug")("eslint:code-path");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Gets id of a given segment.
 * @param {CodePathSegment} segment A segment to get.
 * @returns {string} Id of the segment.
 */
/* c8 ignore next */
// eslint-disable-next-line jsdoc/require-jsdoc -- Ignoring
function getId(segment) {
	return segment.id + (segment.reachable ? "" : "!");
}

/**
 * Get string for the given node and operation.
 * @param {ASTNode} node The node to convert.
 * @param {"enter" | "exit" | undefined} label The operation label.
 * @returns {string} The string representation.
 */
function nodeToString(node, label) {
	const suffix = label ? `:${label}` : "";

	switch (node.type) {
		case "Identifier":
			return `${node.type}${suffix} (${node.name})`;
		case "Literal":
			return `${node.type}${suffix} (${node.value})`;
		default:
			return `${node.type}${suffix}`;
	}
}

/**
 * Escape text for use in a DOT label.
 * @param {string} value The value to escape.
 * @returns {string} The escaped value.
 */
function escapeDotLabelText(value) {
	return value.replace(/\\/gu, String.raw`\\`).replace(/"/gu, String.raw`\"`);
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

module.exports = {
	/**
	 * A flag that debug dumping is enabled or not.
	 * @type {boolean}
	 */
	enabled: debug.enabled,

	/**
	 * Dumps given objects.
	 * @param {...any} args objects to dump.
	 * @returns {void}
	 */
	dump: debug,

	/**
	 * Dumps the current analyzing state.
	 * @param {ASTNode} node A node to dump.
	 * @param {CodePathState} state A state to dump.
	 * @param {boolean} leaving A flag whether or not it's leaving
	 * @returns {void}
	 */
	dumpState: !debug.enabled
		? debug
		: /* c8 ignore next */ function (node, state, leaving) {
				for (let i = 0; i < state.currentSegments.length; ++i) {
					const segInternal = state.currentSegments[i].internal;

					if (leaving) {
						const last = segInternal.nodes.length - 1;

						if (
							last >= 0 &&
							segInternal.nodes[last] ===
								nodeToString(node, "enter")
						) {
							segInternal.nodes[last] = nodeToString(
								node,
								void 0,
							);
						} else {
							segInternal.nodes.push(nodeToString(node, "exit"));
						}
					} else {
						segInternal.nodes.push(nodeToString(node, "enter"));
					}
				}

				debug(
					"%s) %s%s",
					state.currentSegments.map(getId).join(","),
					node.type,
					leaving ? ":exit" : "",
				);
			},

	/**
	 * Dumps a DOT code of a given code path.
	 * The DOT code can be visualized with Graphvis.
	 * @param {CodePath} codePath A code path to dump.
	 * @returns {void}
	 * @see https://www.graphviz.org
	 * @see http://www.webgraphviz.com
	 */
	dumpDot: !debug.enabled
		? debug
		: /* c8 ignore next */ function (codePath) {
				let text =
					"\n" +
					"digraph {\n" +
					'node[shape=box,style="rounded,filled",fillcolor=white];\n' +
					'initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';

				if (codePath.returnedSegments.length > 0) {
					text +=
						'final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n';
				}
				if (codePath.thrownSegments.length > 0) {
					text +=
						'thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true];\n';
				}

				const traceMap = Object.create(null);
				const arrows = this.makeDotArrows(codePath, traceMap);

				// eslint-disable-next-line guard-for-in -- Want ability to traverse prototype
				for (const id in traceMap) {
					const segment = traceMap[id];

					text += `${id}[`;

					if (segment.reachable) {
						text += 'label="';
					} else {
						text +=
							'style="rounded,dashed,filled",fillcolor="#FF9800",label="<<unreachable>>\\n';
					}

					if (segment.internal.nodes.length > 0) {
						text += segment.internal.nodes
							.map(escapeDotLabelText)
							.join("\\n");
					} else {
						text += "????";
					}

					text += '"];\n';
				}

				text += `${arrows}\n`;
				text += "}";
				debug("DOT", text);
			},

	/**
	 * Makes a DOT code of a given code path.
	 * The DOT code can be visualized with Graphvis.
	 * @param {CodePath} codePath A code path to make DOT.
	 * @param {Object} traceMap Optional. A map to check whether or not segments had been done.
	 * @returns {string} A DOT code of the code path.
	 */
	makeDotArrows(codePath, traceMap) {
		const stack = [[codePath.initialSegment, 0]];
		const done = traceMap || Object.create(null);
		let lastId = codePath.initialSegment.id;
		let text = `initial->${codePath.initialSegment.id}`;

		while (stack.length > 0) {
			const item = stack.pop();
			const segment = item[0];
			const index = item[1];

			if (done[segment.id] && index === 0) {
				continue;
			}
			done[segment.id] = segment;

			const nextSegment = segment.allNextSegments[index];

			if (!nextSegment) {
				continue;
			}

			if (lastId === segment.id) {
				text += `->${nextSegment.id}`;
			} else {
				text += `;\n${segment.id}->${nextSegment.id}`;
			}
			lastId = nextSegment.id;

			stack.unshift([segment, 1 + index]);
			stack.push([nextSegment, 0]);
		}

		codePath.returnedSegments.forEach(finalSegment => {
			if (lastId === finalSegment.id) {
				text += "->final";
			} else {
				text += `;\n${finalSegment.id}->final`;
			}
			lastId = null;
		});

		codePath.thrownSegments.forEach(finalSegment => {
			if (lastId === finalSegment.id) {
				text += "->thrown";
			} else {
				text += `;\n${finalSegment.id}->thrown`;
			}
			lastId = null;
		});

		return `${text};`;
	},
};
