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 class App : IApp {
		private static readonly Regex RegexValidCharacters = new Regex(@"^[\s\d\.\-+*/%^]+$", RegexOptions.Compiled);
		private static readonly Regex RegexTokenSeparator = new Regex(@"((?<!\d)-?(((\d+)?\.\d+(\.\.\.)?)|\d+))|[^\d\s]", RegexOptions.ExplicitCapture | RegexOptions.Compiled);
		private static readonly Regex RegexRecurringDecimal = new Regex(@"\b(?:(\d+?\.\d{0,}?)(\d+?)\2+|([\d+?\.]*))\b", RegexOptions.Compiled);

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

		public string[] RecognizedNames => new string[] {
			"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, 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.Substring(0, res.Length - 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.Substring(0, 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.Substring(1);
			}
			else if (str[0] == '.') {
				str = "0" + str;
			}

			if (str.EndsWith("...")) {
				string truncated = str.Substring(0, str.Length - 3);

				if (truncated.IndexOf('.') != -1) {
					str = truncated;
				}
			}

			decimal value;

			if (decimal.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out 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);
			}
		}
	}
}