import stringWidth from "string-width";
import useColors from "@poppinss/colors";
import { inspect, promisify } from "node:util";
import { Recoverable, start } from "node:repl";
const GLOBAL_NODE_PROPERTIES = [
	"performance",
	"global",
	"clearInterval",
	"clearTimeout",
	"setInterval",
	"setTimeout",
	"queueMicrotask",
	"clearImmediate",
	"setImmediate",
	"structuredClone",
	"atob",
	"btoa",
	"fetch",
	"crypto",
	"navigator",
	"localStorage",
	"sessionStorage"
];
const TS_UTILS_HELPERS = [
	"__extends",
	"__assign",
	"__rest",
	"__decorate",
	"__param",
	"__esDecorate",
	"__runInitializers",
	"__propKey",
	"__setFunctionName",
	"__metadata",
	"__awaiter",
	"__generator",
	"__exportStar",
	"__createBinding",
	"__values",
	"__read",
	"__spread",
	"__spreadArrays",
	"__spreadArray",
	"__await",
	"__asyncGenerator",
	"__asyncDelegator",
	"__asyncValues",
	"__makeTemplateObject",
	"__importStar",
	"__importDefault",
	"__classPrivateFieldGet",
	"__classPrivateFieldSet",
	"__classPrivateFieldIn"
];
var Repl = class {
	#replOptions;
	#longestCustomMethodName = 0;
	#originalEval;
	#compiler;
	#historyFilePath;
	#onReadyCallbacks = [];
	#customMethods = {};
	colors = useColors.ansi();
	server;
	constructor(options) {
		const { compiler, historyFilePath, ...rest } = options || {};
		this.#compiler = compiler;
		this.#historyFilePath = historyFilePath;
		this.#replOptions = rest;
	}
	#registerCustomMethodWithContext(name) {
		const customMethod = this.#customMethods[name];
		if (!customMethod) return;
		const handler = (...args) => customMethod.handler(this, ...args);
		Object.defineProperty(handler, "name", { value: customMethod.handler.name });
		this.server.context[name] = handler;
	}
	#setupContext() {
		this.addMethod("clear", function clear(repl, key) {
			if (!key) console.log(repl.colors.red("Define a property name to remove from the context"));
			else delete repl.server.context[key];
			repl.server.displayPrompt();
		}, {
			description: "Clear a property from the REPL context",
			usage: `clear ${this.colors.gray("(propertyName)")}`
		});
		this.addMethod("p", function promisify$1(_, fn) {
			return promisify(fn);
		}, {
			description: "Promisify a function. Similar to Node.js \"util.promisify\"",
			usage: `p ${this.colors.gray("(function)")}`
		});
		Object.keys(this.#customMethods).forEach((name) => {
			this.#registerCustomMethodWithContext(name);
		});
	}
	#isRecoverableError(error) {
		return /^(Unexpected end of input|Unexpected token|' expected)/.test(error.message);
	}
	#eval(code, context, filename, callback) {
		try {
			const compiled = this.#compiler ? this.#compiler.compile(code, filename) : code;
			return this.#originalEval(compiled, context, filename, callback);
		} catch (error) {
			if (this.#isRecoverableError(error)) {
				callback(new Recoverable(error), null);
				return;
			}
			callback(error, null);
		}
	}
	#setupHistory() {
		if (!this.#historyFilePath) return;
		this.server.setupHistory(this.#historyFilePath, (error) => {
			if (!error) return;
			console.log(this.colors.red("Unable to write to the history file. Exiting"));
			console.error(error);
			process.exit(1);
		});
	}
	#printContextHelp() {
		console.log("");
		console.log(this.colors.green("CONTEXT PROPERTIES/METHODS:"));
		const context = Object.keys(this.server.context).reduce((result, key) => {
			if (!this.#customMethods[key] && !GLOBAL_NODE_PROPERTIES.includes(key) && !TS_UTILS_HELPERS.includes(key)) result[key] = this.server.context[key];
			return result;
		}, {});
		console.log(inspect(context, false, 1, true));
	}
	#printCustomMethodsHelp() {
		console.log("");
		console.log(this.colors.green("GLOBAL METHODS:"));
		Object.keys(this.#customMethods).forEach((method) => {
			const { options } = this.#customMethods[method];
			const usage = this.colors.yellow(options.usage || method);
			const spaces = " ".repeat(this.#longestCustomMethodName - options.width + 2);
			const description = this.colors.dim(options.description || "");
			console.log(`${usage}${spaces}${description}`);
		});
	}
	#ls() {
		this.#printCustomMethodsHelp();
		this.#printContextHelp();
		this.server.displayPrompt();
	}
	notify(message) {
		console.log(this.colors.yellow().italic(message));
		if (this.server) this.server.displayPrompt();
	}
	ready(callback) {
		this.#onReadyCallbacks.push(callback);
		return this;
	}
	addMethod(name, handler, options) {
		const width = stringWidth(options?.usage || name);
		if (width > this.#longestCustomMethodName) this.#longestCustomMethodName = width;
		this.#customMethods[name] = {
			handler,
			options: Object.assign({ width }, options)
		};
		if (this.server) this.#registerCustomMethodWithContext(name);
		return this;
	}
	getMethods() {
		return this.#customMethods;
	}
	useCompiler(compiler) {
		this.#compiler = compiler;
		return this;
	}
	start(context) {
		console.log("");
		this.notify("Type \".ls\" to a view list of available context methods/properties");
		this.server = start({
			prompt: `> ${this.#compiler?.supportsTypescript ? "(ts) " : "(js) "}`,
			input: process.stdin,
			output: process.stdout,
			terminal: process.stdout.isTTY && !Number.parseInt(process.env.NODE_NO_READLINE, 10),
			useGlobal: true,
			writer(output) {
				return inspect(output, {
					showProxy: false,
					colors: true
				});
			},
			...this.#replOptions
		});
		if (context) Object.keys(context).forEach((key) => {
			this.server.context[key] = context[key];
		});
		this.server.defineCommand("ls", {
			help: "View list of available context methods/properties",
			action: this.#ls.bind(this)
		});
		this.#setupContext();
		this.#setupHistory();
		this.#originalEval = this.server.eval;
		this.server.eval = this.#eval.bind(this);
		this.server.displayPrompt();
		this.#onReadyCallbacks.forEach((callback) => callback(this));
		return this;
	}
};
export { Repl };
