import { Utils } from '../../lib';

type Operator = '^' | 'negative' | '%' | '/' | '*' | '+' | '-' | '(';
interface Operators {
	precedence: number;
	associativity: "Left" | "Right";
}

const OPERATORS: { [k in Operator]: Operators } = {
	"^": {
		precedence: 5,
		associativity: "Right",
	},
	"negative": {
		precedence: 4,
		associativity: "Right",
	},
	"%": {
		precedence: 3,
		associativity: "Left",
	},
	"/": {
		precedence: 3,
		associativity: "Left",
	},
	"*": {
		precedence: 3,
		associativity: "Left",
	},
	"+": {
		precedence: 2,
		associativity: "Left",
	},
	"-": {
		precedence: 2,
		associativity: "Left",
	},
	"(": {
		precedence: 1,
		associativity: "Right",
	},
};

const BASE_PREFIXES: { [base: number]: string } = {
	2: "0b",
	8: "0o",
	10: "",
	16: "0x",
};

function parseMathematicalExpression(infix: string) {
	// Shunting-yard Algorithm -- https://en.wikipedia.org/wiki/Shunting-yard_algorithm
	const outputQueue: string[] = [];
	const operatorStack: Operator[] = [];
	infix = infix.replace(/\s+/g, "");
	const infixArray = infix.split(/([+\-*/%^()])/).filter(token => token);
	let isExprExpected = true;
	for (const token of infixArray) {
		if (isExprExpected && "+-".includes(token)) {
			if (token === '-') operatorStack.push('negative');
		} else if ("^%*/+-".includes(token)) {
			if (isExprExpected) throw new SyntaxError(`Got "${token}" where an expression should be`);
			const op = OPERATORS[token as Operator];
			let prevToken = operatorStack[operatorStack.length - 1] || '(';
			let prevOp = OPERATORS[prevToken];
			while (op.associativity === "Left" ? op.precedence <= prevOp.precedence : op.precedence < prevOp.precedence) {
				outputQueue.push(operatorStack.pop()!);
				prevToken = operatorStack[operatorStack.length - 1] || '(';
				prevOp = OPERATORS[prevToken];
			}
			operatorStack.push(token as Operator);
			isExprExpected = true;
		} else if (token === "(") {
			if (!isExprExpected) throw new SyntaxError(`Got "(" where an operator should be`);
			operatorStack.push(token as Operator);
			isExprExpected = true;
		} else if (token === ")") {
			if (isExprExpected) throw new SyntaxError(`Got ")" where an expression should be`);
			while (operatorStack.length && operatorStack[operatorStack.length - 1] !== "(") {
				outputQueue.push(operatorStack.pop()!);
			}
			operatorStack.pop();
			isExprExpected = false;
		} else {
			if (!isExprExpected) throw new SyntaxError(`Got "${token}" where an operator should be`);
			outputQueue.push(token);
			isExprExpected = false;
		}
	}
	if (isExprExpected) throw new SyntaxError(`Input ended where an expression should be`);
	while (operatorStack.length > 0) {
		const token = operatorStack.pop()!;
		if (token === '(') continue;
		outputQueue.push(token);
	}
	return outputQueue;
}

function solveRPN(rpn: string[]): [number, number] {
	let base = 10;
	const resultStack: number[] = [];
	for (let token of rpn) {
		if (token === 'negative') {
			if (!resultStack.length) throw new SyntaxError(`Unknown syntax error`);
			resultStack.push(-resultStack.pop()!);
		} else if (!"^%*/+-".includes(token)) {
			if (token.endsWith('h')) {
				// Convert h suffix for hexadecimal to 0x prefix
				token = `0x${token.slice(0, -1)}`;
			} else if (token.endsWith('o')) {
				// Convert o suffix for octal to 0o prefix
				token = `0o${token.slice(0, -1)}`;
			} else if (token.endsWith('b')) {
				// Convert b suffix for binary to 0b prefix
				token = `0b${token.slice(0, -1)}`;
			}
			if (token.startsWith('0x')) base = 16;
			if (token.startsWith('0b')) base = 2;
			if (token.startsWith('0o')) base = 8;
			let num = Number(token);
			if (isNaN(num) && token.toUpperCase() in Math) {
				// @ts-expect-error Math consts should be safe
				num = Math[token.toUpperCase()];
			}
			if (isNaN(num) && token !== 'NaN') {
				throw new SyntaxError(`Unrecognized token ${token}`);
			}
			resultStack.push(num);
		} else {
			if (resultStack.length < 2) throw new SyntaxError(`Unknown syntax error`);
			const a = resultStack.pop()!;
			const b = resultStack.pop()!;
			switch (token) {
			case "+":
				resultStack.push(a + b);
				break;
			case "-":
				resultStack.push(b - a);
				break;
			case "*":
				resultStack.push(a * b);
				break;
			case "/":
				resultStack.push(b / a);
				break;
			case "%":
				resultStack.push(b % a);
				break;
			case "^":
				resultStack.push(b ** a);
				break;
			}
		}
	}
	if (resultStack.length !== 1) throw new SyntaxError(`Unknown syntax error`);
	return [resultStack.pop()!, base];
}

export const commands: Chat.ChatCommands = {
	math: "calculate",
	calculate(target, room, user) {
		if (!target) return this.parse('/help calculate');

		let base = 0;
		const baseMatchResult = (/\b(?:in|to)\s+([a-zA-Z]+)\b/).exec(target);
		if (baseMatchResult) {
			switch (toID(baseMatchResult[1])) {
			case 'decimal': case 'dec': base = 10; break;
			case 'hexadecimal': case 'hex': base = 16; break;
			case 'octal': case 'oct': base = 8; break;
			case 'binary': case 'bin': base = 2; break;
			default:
				throw new Chat.ErrorMessage(`Unrecognized base "${baseMatchResult[1]}". Valid options are binary or bin, octal or oct, decimal or dec, and hexadecimal or hex.`);
			}
		}
		const expression = target.replace(/\b(in|to)\s+([a-zA-Z]+)\b/g, '').trim();

		if (!this.runBroadcast()) return;
		try {
			const [result, inferredBase] = solveRPN(parseMathematicalExpression(expression));
			if (!base) base = inferredBase;
			let baseResult = '';
			if (Number.isFinite(result) && base !== 10) {
				baseResult = `${BASE_PREFIXES[base]}${result.toString(base).toUpperCase()}`;
				if (baseResult === expression) baseResult = '';
			}
			let resultStr = '';
			const resultTruncated = parseFloat(result.toPrecision(15));
			let resultDisplay = resultTruncated.toString();
			if (resultTruncated > 10 ** 15) {
				resultDisplay = resultTruncated.toExponential();
			}
			if (baseResult) {
				resultStr = `<strong>${baseResult}</strong> = ${resultDisplay}`;
			} else {
				resultStr = `<strong>${resultDisplay}</strong>`;
			}
			this.sendReplyBox(`${expression}<br />= ${resultStr}`);
		} catch (e: any) {
			this.sendReplyBox(
				Utils.html`${expression}<br />= <span class="message-error"><strong>Invalid input:</strong> ${e.message}</span>`
			);
		}
	},
	calculatehelp: [
		`/calculate [arithmetic question] - Calculates an arithmetical question. Supports PEMDAS (Parenthesis, Exponents, Multiplication, Division, Addition and Subtraction), pi and e.`,
		`/calculate [arithmetic question] in [base] - Returns the result in a specific base. [base] can be bin, oct, dec or hex.`,
	],
};
