using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace GraphingCalculator
{
    public class Expression
    {
        #region Types
        public enum TokenType
        {
            None, Literal, Identifier, Operator, ArgSeparator,
            LParanthesis, RParanthesis, Function
        };
        /// 
        /// Represents a token
        /// 
        protected class Token
        {
            public TokenType Type { get; set; }
            public string Text { get; set; }
            public Token(TokenType type, string text)
            {
                Type = type;
                Text = text;
            }
        }
        
        #endregion
        #region Variables, properties
        
        private string expressionString;
        private bool parsed = false;
        protected List Tokens { get; private set; }
        protected List Postfix { get; private set; }
        public Dictionary Variables { get; private set; }
        public string ExpressionString
        {
            get { return expressionString; }
            set
            {
                expressionString = value;
                parsed = false;
            }
        }
        #endregion
        #region Other stuff
        public void AddVariable(string name, float value)
        {
            Variables.Add(name, value);
        }
        #endregion
        #region Constructor
        public Expression()
        {
            Tokens = new List();
            Postfix = new List();
            Variables = new Dictionary();
            Variables.Add("pi", Mathf.PI);
        }
        public Expression(string expr)
            : this()
        {
            expressionString = expr;
        }
        #endregion
        #region Parser
        private void AnalyzeLex()
        {
            Tokens.Clear();
            for (int i = 0; i < ExpressionString.Length; i++)
            {
                if (char.IsWhiteSpace(ExpressionString[i])) continue;
                if (IsOperator(ExpressionString[i]))
                {
                    // Handle unary minus
                    if (IsUnaryMinus(ExpressionString[i]))
                        Tokens.Add(new Token(TokenType.Operator, "u"));
                    else
                        Tokens.Add(new Token(TokenType.Operator, ExpressionString[i].ToString()));
                }
                else if (ExpressionString[i] == '(')
                    Tokens.Add(new Token(TokenType.LParanthesis, ExpressionString[i].ToString()));
                else if (ExpressionString[i] == ')')
                    Tokens.Add(new Token(TokenType.RParanthesis, ExpressionString[i].ToString()));
                else if (ExpressionString[i] == ',')
                    Tokens.Add(new Token(TokenType.ArgSeparator, ExpressionString[i].ToString()));
                else if (Char.IsDigit(ExpressionString[i]))
                    Tokens.Add(new Token(TokenType.Literal, GetLiteral(ExpressionString, ref i)));
                else if (Char.IsLetter(ExpressionString[i]))
                    Tokens.Add(new Token(TokenType.Identifier, GetIdentifier(ExpressionString, ref i)));
                else throw new Exception("Unrecognized character found!");
            }
        }
        private void ConvertPostfix()
        {
            Stack stack = new Stack();
            for (int i = 0; i < Tokens.Count; i++)
            {
                Token t = Tokens[i];
                switch (t.Type)
                {
                    case TokenType.Identifier:
                        
                        // Followed by '(' means function
                        if (i + 1 < Tokens.Count && Tokens[i + 1].Type == TokenType.LParanthesis)
                            stack.Push(new Token(TokenType.Function, t.Text));
                        // Else, variable
                        else Postfix.Add(t);
                        
                        break;
                    case TokenType.Literal: 
                        Postfix.Add(t);
                        break;
                    case TokenType.ArgSeparator:
                        // We pop everything from the stack until left paranthesis open
                        while (stack.Peek().Type != TokenType.LParanthesis)
                        {
                            Postfix.Add(stack.Pop());
                            
                            if (stack.Count == 0) 
                                throw new Exception("Syntax error! Unexpected comma.");
                        }
                        break;
                    case TokenType.Operator:
                        if (IsLeftAssociative(t.Text))
                        {
                            while (stack.Count != 0 && Precedence(t.Text) <= Precedence(stack.Peek().Text))
                                Postfix.Add(stack.Pop());
                        }
                        else
                        {
                            while (stack.Count != 0 && Precedence(t.Text) < Precedence(stack.Peek().Text))
                                Postfix.Add(stack.Pop());
                        }
                        stack.Push(t);
                        break;
                    case TokenType.LParanthesis: 
                        stack.Push(t); 
                        break;
                    case TokenType.RParanthesis:
                        while (stack.Peek().Type != TokenType.LParanthesis)
                        {
                            Postfix.Add(stack.Pop());
                            
                            if (stack.Count == 0) 
                                throw new Exception("Mismatched parantheses!");
                        }
                        stack.Pop(); // Pop Lparanthesis
                        if (stack.Count > 0 && stack.Peek().Type == TokenType.Function) 
                            Postfix.Add(stack.Pop());   // Pop function name
                        break;
                }
            }
            while (stack.Count > 0)
            {
                if (stack.Peek().Type == TokenType.LParanthesis)
                    throw new Exception("Mismatched parantheses!");
                
                Postfix.Add(stack.Pop());
            }
        }
        public void ParseExpression()
        {
            if (!parsed)
            {
                Tokens.Clear();
                Postfix.Clear();
                AnalyzeLex();
                ConvertPostfix();
            }
        }
        public float Evaluate()
        {
            // Parse expression first
            ParseExpression();
            
            // Expression is empty, so is result
            if (Postfix.Count == 0) 
                return 0;
            Stack stack = new Stack();
            foreach (var t in Postfix)
            {
                switch (t.Type)
                {
                    // We already replace functions, so identifiers are variables
                    case TokenType.Identifier:
                        
                        if (!Variables.ContainsKey(t.Text))
                            throw new Exception("Undefined variable '" + t.Text + "'.");
                        stack.Push(Variables[t.Text]);
                        break;
                    case TokenType.Literal: 
                        stack.Push(float.Parse(t.Text)); 
                        break;
                    case TokenType.Operator:
                        switch (t.Text)
                        {
                            case "u": 
                                stack.Push(stack.Pop() * -1); 
                                break;
                            case "+": 
                                stack.Push(stack.Pop() + stack.Pop());
                                break;
                            case "-":
                                {
                                    float b = stack.Pop();
                                    float a = stack.Pop();
                                    stack.Push(a - b);
                                } 
                                break;
                            case "*": 
                                stack.Push(stack.Pop() * stack.Pop()); 
                                break;
                            case "/":
                                {
                                    float b = stack.Pop();
                                    float a = stack.Pop();
                                    stack.Push(a / b);
                                }
                                break;
                            case "%":
                                {
                                    float b = stack.Pop();
                                    float a = stack.Pop();
                                    stack.Push(a % b);
                                }
                                break;
                            case "^":
                                {
                                    float b = stack.Pop();
                                    float a = stack.Pop();
                                    stack.Push(Mathf.Pow(a, b));
                                }
                                break;
                        }
                        break;
                    case TokenType.Function:
                        EvaluateFunction(t.Text, ref stack);
                        break;
                }
            }
            return stack.Pop();
        }
        #endregion
        #region Helper routines
        private bool IsUnaryMinus(char c)
        {
            if (c == '-')
            {
                // Nothing in front, definitely unary
                if (Tokens.Count == 0)
                    return true;
                // See what's in front
                TokenType inFront = Tokens.Last().Type;
                // If what's in front cannot be an operand, than it is unary minus
                return inFront == TokenType.ArgSeparator || inFront == TokenType.LParanthesis || inFront == TokenType.Operator;
            }
            return false;
        }
        private void EvaluateFunction(string func, ref Stack stack)
        {
            switch (func)
            {
                case "sin": stack.Push(Mathf.Sin(stack.Pop())); break;
                case "cos": stack.Push(Mathf.Cos(stack.Pop())); break;
                case "tan": stack.Push(Mathf.Tan(stack.Pop())); break;
                case "ctan": stack.Push(1 / Mathf.Tan(stack.Pop())); break;
                case "arcsin": stack.Push(Mathf.Asin(stack.Pop())); break;
                case "asin": stack.Push(Mathf.Asin(stack.Pop())); break;
                case "arccos": stack.Push(Mathf.Acos(stack.Pop())); break;
                case "acos": stack.Push(Mathf.Acos(stack.Pop())); break;
                case "arctan": stack.Push(Mathf.Atan(stack.Pop())); break;
                case "atan": stack.Push(Mathf.Atan(stack.Pop())); break;
                case "truncate":
                case "floor": stack.Push(Mathf.Floor(stack.Pop())); break;
                case "ceil":
                case "ceiling": stack.Push(Mathf.Ceil(stack.Pop())); break;
                case "sqrt": stack.Push(Mathf.Sqrt(stack.Pop())); break;
                case "cbrt": stack.Push(Mathf.Pow(stack.Pop(), 1.0f / 3.0f)); break;
                case "root": stack.Push(Mathf.Pow(stack.Pop(), 1 / stack.Pop())); break;
                case "abs": stack.Push(Math.Abs(stack.Pop())); break;
                case "max": stack.Push(Math.Max(stack.Pop(), stack.Pop())); break;
                case "min": stack.Push(Math.Min(stack.Pop(), stack.Pop())); break;
                case "lg": stack.Push(Mathf.Log10(stack.Pop())); break;
                case "log": stack.Push(Mathf.Log(stack.Pop(), stack.Pop())); break;
                
                case "clamp01": stack.Push(Mathf.Clamp01(stack.Pop())); break;
                case "clamp": stack.Push(Mathf.Clamp(stack.Pop(), stack.Pop(), stack.Pop())); break;
                case "lerp": stack.Push(Mathf.Lerp(stack.Pop(), stack.Pop(), stack.Pop())); break;
                default: throw new Exception("Undefined function '" + func + "'.");
            }
        }
        private static bool IsLeftAssociative(string op)
        {
            return (op != "^");
        }
        private static int Precedence(string op)
        {
            switch (op)
            {
                case "+":
                case "-": return 1;
                case "*":
                case "/":
                case "%": return 2;
                case "u":
                case "^": return 3;
                default: return 0;
            }
        }
        private static bool IsOperator(char c)
        {
            const string operators = "+-*/%^";
            return operators.Contains(c);
        }
        private static string GetIdentifier(string s, ref int index)
        {
            int start = index;
            while (index < s.Length && (char.IsLetterOrDigit(s[index]) || s[index] == '_'))
                ++index;
            index -= 1;
            return s.Substring(start, index + 1 - start);
        }
        private static string GetLiteral(string s, ref int index)
        {
            int start = index;
            while (index < s.Length && (char.IsDigit(s[index]) || s[index] == '.'))
                ++index;
            index -= 1;
            return s.Substring(start, index + 1 - start);
        }
        #endregion
    }
}