using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Base;

namespace AppCalc;

public sealed partial class App : IApp {
	private static readonly Regex RegexValidCharacters = GetRegexValidCharacters();
	private static readonly Regex RegexTokenSeparator = GetRegexTokenSeparator();
	private static readonly Regex RegexRecurringDecimal = GetRegexRecurringDecimal();

	[GeneratedRegex(@"^[\s\d\.\-+*/%^]+$", RegexOptions.Compiled)]
	private static partial Regex GetRegexValidCharacters();

	[GeneratedRegex(@"((?<!\d)-?(((\d+)?\.\d+(\.\.\.)?)|\d+))|[^\d\s]", RegexOptions.ExplicitCapture | RegexOptions.Compiled)]
	private static partial Regex GetRegexTokenSeparator();

	[GeneratedRegex(@"\b(?:(\d+?\.\d{0,}?)(\d+?)\2+|([\d+?\.]*))\b", RegexOptions.Compiled)]
	private static partial Regex GetRegexRecurringDecimal();

	private static readonly char[] SplitSpace = [ ' ' ];

	public string[] RecognizedNames => [
		"calc",
		"calculate",
		"calculator"
	];

	public MatchConfidence GetConfidence(Command cmd) {
		return RegexValidCharacters.IsMatch(cmd.Text) ? MatchConfidence.Possible : MatchConfidence.None;
	}

	string? IApp.ProcessCommand(Command cmd) {
		return ParseAndProcessExpression(cmd.Text);
	}

	private static string ParseAndProcessExpression(string text) {
		// text = RegexBalancedParentheses.Replace(text, match => " "+ParseAndProcessExpression(match.Groups[1].Value)+" "); // parens are handled as apps

		string expression = RegexTokenSeparator.Replace(text, static match => " " + match.Value + " ");
		string[] tokens = expression.Split(SplitSpace, StringSplitOptions.RemoveEmptyEntries);

		decimal result = ProcessExpression(tokens);

		if (Math.Abs(result - decimal.Round(result)) < 0.0000000000000000000000000010M) {
			return decimal.Round(result).ToString(CultureInfo.InvariantCulture);
		}

		string res = result.ToString(CultureInfo.InvariantCulture);
		bool hasDecimalPoint = decimal.Round(result) != result;

		if (res.Length == 30 && hasDecimalPoint && res.IndexOf('.') < 15) { // Length 30 uses all available bytes
			res = res[..^1];

			Match match = RegexRecurringDecimal.Match(res);

			if (match.Groups[2].Success) {
				string repeating = match.Groups[2].Value;

				StringBuilder build = new StringBuilder(34);
				build.Append(match.Groups[1].Value);

				do {
					build.Append(repeating);
				} while (build.Length + repeating.Length <= res.Length);

				return build.Append(repeating[..(1 + build.Length - res.Length)]).Append("...").ToString();
			}
		}
		else if (hasDecimalPoint) {
			res = res.TrimEnd('0');
		}

		return res;
	}

	private static decimal ProcessExpression(string[] tokens) {
		bool isPostfix;

		if (tokens.Length < 3) {
			isPostfix = false;
		}
		else {
			try {
				ParseNumberToken(tokens[0]);
			} catch (CommandException) {
				throw new CommandException("Prefix notation is not supported.");
			}

			try {
				ParseNumberToken(tokens[1]);
				isPostfix = true;
			} catch (CommandException) {
				isPostfix = false;
			}
		}

		if (isPostfix) {
			return ProcessPostfixExpression(tokens);
		}
		else {
			return ProcessPostfixExpression(ConvertInfixToPostfix(tokens));
		}
	}

	private static IEnumerable<string> ConvertInfixToPostfix(IEnumerable<string> tokens) {
		Stack<string> operators = new Stack<string>();

		foreach (string token in tokens) {
			if (Operators.With2Operands.Contains(token)) {
				int currentPrecedence = Operators.GetPrecedence(token);
				bool currentRightAssociative = Operators.IsRightAssociative(token);

				while (operators.Count > 0) {
					int topPrecedence = Operators.GetPrecedence(operators.Peek());

					if ((currentRightAssociative && currentPrecedence < topPrecedence) || (!currentRightAssociative && currentPrecedence <= topPrecedence)) {
						yield return operators.Pop();
					}
					else {
						break;
					}
				}

				operators.Push(token);
			}
			else {
				yield return ParseNumberToken(token).ToString(CultureInfo.InvariantCulture);
			}
		}

		while (operators.Count > 0) {
			yield return operators.Pop();
		}
	}

	private static decimal ProcessPostfixExpression(IEnumerable<string> tokens) {
		Stack<decimal> stack = new Stack<decimal>();

		foreach (string token in tokens) {
			decimal operand1, operand2;

			if (token == "-" && stack.Count == 1) {
				operand2 = stack.Pop();
				operand1 = 0M;
			}
			else if (Operators.With2Operands.Contains(token)) {
				if (stack.Count < 2) {
					throw new CommandException("Incorrect syntax, not enough numbers in stack.");
				}

				operand2 = stack.Pop();
				operand1 = stack.Pop();
			}
			else {
				operand1 = operand2 = 0M;
			}

			switch (token) {
				case "+":
					stack.Push(operand1 + operand2);
					break;
				case "-":
					stack.Push(operand1 - operand2);
					break;
				case "*":
					stack.Push(operand1 * operand2);
					break;

				case "/":
					if (operand2 == 0M) {
						throw new CommandException("Cannot divide by zero.");
					}

					stack.Push(operand1 / operand2);
					break;

				case "%":
					if (operand2 == 0M) {
						throw new CommandException("Cannot divide by zero.");
					}

					stack.Push(operand1 % operand2);
					break;

				case "^":
					if (operand1 == 0M && operand2 == 0M) {
						throw new CommandException("Cannot evaluate 0 to the power of 0.");
					}
					else if (operand1 < 0M && Math.Abs(operand2) < 1M) {
						throw new CommandException("Cannot evaluate a root of a negative number.");
					}

					try {
						stack.Push((decimal) Math.Pow((double) operand1, (double) operand2));
					} catch (OverflowException ex) {
						throw new CommandException("Number overflow.", ex);
					}

					break;

				default:
					stack.Push(ParseNumberToken(token));
					break;
			}
		}

		if (stack.Count != 1) {
			throw new CommandException("Incorrect syntax, too many numbers in stack.");
		}

		return stack.Pop();
	}

	private static decimal ParseNumberToken(string token) {
		string str = token;

		if (str.StartsWith("-.")) {
			str = "-0" + str[1..];
		}
		else if (str[0] == '.') {
			str = "0" + str;
		}

		if (str.EndsWith("...")) {
			string truncated = str[..^3];

			if (truncated.Contains('.')) {
				str = truncated;
			}
		}

		if (decimal.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal value)) {
			if (value.ToString(CultureInfo.InvariantCulture) != str) {
				throw new CommandException("Provided number is outside of decimal range: " + token);
			}

			return value;
		}
		else {
			throw new CommandException("Invalid token, expected a number: " + token);
		}
	}
}