using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace FMSAdmin.Helpers.Formula {
    public class MathCalculator {
        public enum Parameter {
            A = 0, B, C, D, E, F, G, H, I,
            J, K, L, M, N, O, P, Q, R,
            S, T, U, V, W, X, Y, Z
        }
        public enum OperatorType {
            Plus,
            Minus,
            Multiplication,
            Division,
            OpenBrace
        }

        public delegate decimal CalculateFunctionDelegate(decimal inputValue);

        public Dictionary<Parameter, MathParameterValue> Parameters = new Dictionary<Parameter, MathParameterValue>();
        public Dictionary<String, CalculateFunctionDelegate> Functions = new Dictionary<String, CalculateFunctionDelegate>();

        private List<String> OperationOrder = new List<string>();

        private static Regex doubleRegex = new Regex(@"^-?\d+(?:\.\d+)?", RegexOptions.Compiled);
        private static Regex functionRegex = new Regex(@"^\$(.+?)\((.)\)\$", RegexOptions.Compiled);

        private String Fomula { get; set; }
        Stack<FormulaItem> stack = new Stack<FormulaItem>();

        List<FormulaItem> CompliedFormula = new List<FormulaItem>();

        public void TracePostFormula() {
            foreach (var item in this.CompliedFormula) {
            }
        }

        public void SetParameter(MathParameterValue[] parameters) {
            foreach (var p in parameters) {
                this.Parameters.Add(p.Variable, p);
            }
            MapParameters();
        }
        public void SetFunction(string functionName, CalculateFunctionDelegate callback) {

            if (this.Functions.ContainsKey(functionName)) {
                this.Functions[functionName] = callback;
                return;
            }

            this.Functions.Add(functionName, callback);

            MapFunctions();
        }

        public List<FunctionItem> GetFunctions() {
            var list = new List<FunctionItem>();
            foreach (var item in this.CompliedFormula) {
                switch (item.Type) {
                    case FormulaItem.ItemType.Function:
                        list.Add((FunctionItem)item);
                        break;
                }

            }

            return list;
        }


        private void MapFunctions() {
            foreach (var item in this.CompliedFormula) {
                switch (item.Type) {
                    case FormulaItem.ItemType.Function:
                        var f = item as FunctionItem;
                        f.Function.FunctionDelegate = this.Functions[f.Function.FunctionName];
                        break;
                }

            }
        }

        private void MapParameters() {
            foreach (var item in this.CompliedFormula) {
                switch (item.Type) {
                    case FormulaItem.ItemType.OperandField:
                        var o = item as OperandFieldItem;
                        o.ParameterValue = this.Parameters[o.ParameterValue.Variable];
                        break;
                    case FormulaItem.ItemType.Function:
                        var f = item as FunctionItem;
                        switch (f.Value.Type) {
                            case FormulaItem.ItemType.OperandField:
                                var of = (OperandFieldItem)f.Value;
                                of.ParameterValue = this.Parameters[of.ParameterValue.Variable];
                                break;
                        }
                        break;
                }

            }
        }

        public decimal? Calculate() {
            Stack<FormulaItem> stack = new Stack<FormulaItem>();
            foreach (var item in this.CompliedFormula) {
                switch (item.Type) {
                    case FormulaItem.ItemType.Constant:
                    case FormulaItem.ItemType.OperandField:
                    case FormulaItem.ItemType.Function:
                        stack.Push(item);
                        break;

                    case FormulaItem.ItemType.Operator:
                        var ci = this.CalculateOperator(stack.Pop(), stack.Pop(), ((OperatorItem)item).Operator);
                        if (ci == null) {
                            return null;
                        }
                        stack.Push(ci);
                        break;
                }
            }
            return stack.Pop().GetValue();
        }

        private ConstantItem CalculateOperator(FormulaItem item1, FormulaItem item2, OperatorType op) {
            decimal result = 0;

            switch (op) {
                case OperatorType.Plus:
                    result = item2.GetValue() + item1.GetValue();
                    break;
                case OperatorType.Minus:
                    result = item2.GetValue() - item1.GetValue();
                    break;
                case OperatorType.Multiplication:
                    result = item2.GetValue() * item1.GetValue();
                    break;
                case OperatorType.Division:
                    if (item1.GetValue() == 0)
                        return null;
                    result = item2.GetValue() / item1.GetValue();
                    break;
            }
            return new ConstantItem { Value = result };
        }

        public bool Compile(string formula) {
            this.CompliedFormula.Clear();

            formula = formula.Replace(" ", "");
            Stack<OperatorItem> stack = new Stack<OperatorItem>();
            char[] braces = new char[] { '(', ')' };
            string operators = "+-*/";
            var array = formula.ToCharArray();
            int runcount = 0;
            for (var i = 0; i < array.Length;) {
                if (++runcount > 1000) break;
                var item = array[i];
                switch (item) {
                    case '(':
                        stack.Push(new OperatorItem { Operator = OperatorType.OpenBrace });
                        i++;
                        break;
                    case ')':
                        while (stack.Any()) {
                            var stackItem = stack.Pop();

                            if (stackItem.Operator == OperatorType.OpenBrace) {
                                break;
                            }
                            CompliedFormula.Add(stackItem);
                        }
                        i++;
                        break;
                    case '$':
                        Match funcMatch = functionRegex.Match(formula.Substring(i));
                        if (funcMatch.Success == false || funcMatch.Groups.Count != 3) {
                            CompliedFormula.Clear();
                            return false;
                        }

                        var f = new FunctionItem {
                            Function = new MathFunction { FunctionName = funcMatch.Groups[1].Value }
                        };


                        string value = funcMatch.Groups[2].Value;
                        if ('A' <= value[0] && value[0] <= 'Z') {
                            if (value.Length > 1) {
                                return false;
                            }
                            f.Value = new OperandFieldItem { ParameterValue = new MathParameterValue { Variable = (Parameter)(value[0] - 'A') } };
                        } else {
                            decimal outputValue;
                            if (decimal.TryParse(value, out outputValue) == false) {
                                return false;
                            }
                            f.Value = new ConstantItem { Value = outputValue };
                        }

                        CompliedFormula.Add(f);

                        i += funcMatch.Length;
                        break;

                    default:
                        int operatorIndex = operators.IndexOf(item);
                        bool isProcessed = false;
                        if (operatorIndex == (int)OperatorType.Minus) {
                            if (i == 0 || array[i - 1] == '(' || operators.IndexOf(array[i - 1]) >= 0) {
                                isProcessed = true;
                            }
                        }
                        if (isProcessed == false) {
                            if (operatorIndex >= 0) {
                                //if( operatorIndex < (int)OperatorType.Multiplication )
                                {
                                    while (stack.Any()) {
                                        var stackItem = stack.Peek();
                                        if (stackItem.Operator == OperatorType.OpenBrace) break;
                                        if ((int)stackItem.Operator < operatorIndex) break;

                                        stack.Pop();
                                        CompliedFormula.Add(stackItem);
                                    }
                                }
                                stack.Push(new OperatorItem { Operator = (OperatorType)operatorIndex });
                                i++;
                                break;
                            } else if ('A' <= item && item <= 'Z') {
                                //Trace.WriteLine( item );
                                CompliedFormula.Add(new OperandFieldItem { ParameterValue = new MathParameterValue { Variable = (Parameter)(item - 'A') } });
                                i++;
                                break;
                            }
                        }
                        Match match = doubleRegex.Match(formula.Substring(i));
                        if (match.Success == false) {
                            throw new Exception("Error: Check Syntax : " + item);
                        }

                        decimal parseValue = decimal.Parse(match.Value);
                        //Trace.WriteLine( value );
                        CompliedFormula.Add(new ConstantItem { Value = parseValue });
                        i += match.Value.Length;
                        break;
                }
            }
            while (stack.Any()) {
                var operatorItem = stack.Pop();
                //Trace.WriteLine( operatorItem.Operator );
                CompliedFormula.Add(operatorItem);
            }

            return true;
        }

        public bool ExtractDouble(string inputValue, out decimal output) {
            return decimal.TryParse(inputValue, out output);
        }

        public abstract class FormulaItem {
            public enum ItemType {
                Operator,
                Constant,
                OperandField,
                Function
            }

            public ItemType Type { get; set; }

            public abstract decimal GetValue();
        }

        public class OperatorItem : FormulaItem {

            public OperatorType Operator { get; set; }

            public OperatorItem() {
                this.Type = FormulaItem.ItemType.Operator;
            }

            public override string ToString() {
                switch (this.Operator) {
                    case OperatorType.Plus:
                        return "+";
                    case OperatorType.Minus:
                        return "-";
                    case OperatorType.Multiplication:
                        return "*";
                    case OperatorType.Division:
                        return "/";
                }
                return "Unknown";
            }
            public override decimal GetValue() {
                throw new Exception("OperatorItem.GetValue() Called!");
                //return 0;
            }

        }

        public class ConstantItem : FormulaItem {
            public decimal Value { get; set; }

            public ConstantItem() {
                this.Type = FormulaItem.ItemType.Constant;
            }

            public override string ToString() {
                return this.Value.ToString();
            }
            public override decimal GetValue() {
                return this.Value;
            }
        }

        public class OperandFieldItem : FormulaItem {
            public MathParameterValue ParameterValue { get; set; }

            public OperandFieldItem() {
                this.Type = FormulaItem.ItemType.OperandField;
            }

            public override string ToString() {
                return this.ParameterValue.Variable.ToString();
            }

            public override decimal GetValue() {
                return this.ParameterValue.Value;
            }
        }

        public class FunctionItem : FormulaItem {
            public MathFunction Function { get; set; }
            public FormulaItem Value { get; set; }

            public FunctionItem() {
                this.Type = FormulaItem.ItemType.Function;
            }

            public override string ToString() {
                return string.Format("Function({0})", this.Value.GetValue());
            }

            public override decimal GetValue() {
                return this.Function.FunctionDelegate(this.Value.GetValue());
            }
        }
    }

    public class MathFunction {
        public String FunctionName { get; set; }
        public MathCalculator.CalculateFunctionDelegate FunctionDelegate { get; set; }
    }
    public class MathParameterValue {
        public MathCalculator.Parameter Variable { get; set; }
        public decimal Value { get; set; }
    }
}
