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);
            }
        }
    }
}