using System;
using System.Collections.Generic;
using System.Linq;
namespace Swan.Parsers
{
    /// 
    /// Represents a generic tokenizer.
    /// 
    public abstract class Tokenizer
    {
        private const char PeriodChar = '.';
        private const char CommaChar = ',';
        private const char StringQuotedChar = '"';
        private const char OpenFuncChar = '(';
        private const char CloseFuncChar = ')';
        private const char NegativeChar = '-';
        private const string OpenFuncStr = "(";
        private readonly List _operators = new List();
        /// 
        /// Initializes a new instance of the  class.
        /// This constructor will use the following default operators:
        ///
        /// 
        ///     
        ///     Operator
        ///     Precedence
        ///     
        /// - 
        /// =
        /// 1
        /// 
 
        /// - 
        /// !=
        /// 1
        /// 
 
        /// - 
        /// >
        /// 2
        /// 
 
        /// - 
        /// <
        /// 2
        /// 
 
        /// - 
        /// >=
        /// 2
        /// 
 
        /// - 
        /// <=
        /// 2
        /// 
 
        /// - 
        /// +
        /// 3
        /// 
 
        /// - 
        /// &
        /// 3
        /// 
 
        /// - 
        /// -
        /// 3
        /// 
 
        /// - 
        /// *
        /// 4
        /// 
 
        /// - 
        /// (backslash)
        /// 4
        /// 
 
        /// - 
        /// /
        /// 4
        /// 
 
        /// - 
        /// ^
        /// 4
        /// 
 
        /// 
        /// 
        /// The input.
        protected Tokenizer(string input)
        {
            _operators.AddRange(GetDefaultOperators());
            Tokenize(input);
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The input.
        /// The operators to use.
        protected Tokenizer(string input, IEnumerable operators)
        {
            _operators.AddRange(operators);
            Tokenize(input);
        }
        /// 
        /// Gets the tokens.
        /// 
        /// 
        /// The tokens.
        /// 
        public List Tokens { get; } = new List();
        /// 
        /// Validates the input and return the start index for tokenizer.
        /// 
        /// The input.
        /// The start index.
        /// true if the input is valid, otherwise false.
        public abstract bool ValidateInput(string input, out int startIndex);
        /// 
        /// Resolves the type of the function or member.
        /// 
        /// The input.
        /// The token type.
        public abstract TokenType ResolveFunctionOrMemberType(string input);
        /// 
        /// Evaluates the function or member.
        /// 
        /// The input.
        /// The position.
        /// true if the input is a valid function or variable, otherwise false.
        public virtual bool EvaluateFunctionOrMember(string input, int position) => false;
        /// 
        /// Gets the default operators.
        /// 
        /// An array with the operators to use for the tokenizer.
        public virtual Operator[] GetDefaultOperators() => new[]
        {
            new Operator {Name = "=", Precedence = 1},
            new Operator {Name = "!=", Precedence = 1},
            new Operator {Name = ">", Precedence = 2},
            new Operator {Name = "<", Precedence = 2},
            new Operator {Name = ">=", Precedence = 2},
            new Operator {Name = "<=", Precedence = 2},
            new Operator {Name = "+", Precedence = 3},
            new Operator {Name = "&", Precedence = 3},
            new Operator {Name = "-", Precedence = 3},
            new Operator {Name = "*", Precedence = 4},
            new Operator {Name = "/", Precedence = 4},
            new Operator {Name = "\\", Precedence = 4},
            new Operator {Name = "^", Precedence = 4},
        };
        /// 
        /// Shunting the yard.
        /// 
        /// if set to true [include function stopper] (Token type Wall).
        /// 
        /// Enumerable of the token in in.
        /// 
        /// 
        /// Wrong token
        /// or
        /// Mismatched parenthesis.
        /// 
        public virtual IEnumerable ShuntingYard(bool includeFunctionStopper = true)
        {
            var stack = new Stack();
            foreach (var tok in Tokens)
            {
                switch (tok.Type)
                {
                    case TokenType.Number:
                    case TokenType.Variable:
                    case TokenType.String:
                        yield return tok;
                        break;
                    case TokenType.Function:
                        stack.Push(tok);
                        break;
                    case TokenType.Operator:
                        while (stack.Any() && stack.Peek().Type == TokenType.Operator &&
                               CompareOperators(tok.Value, stack.Peek().Value))
                            yield return stack.Pop();
                        stack.Push(tok);
                        break;
                    case TokenType.Comma:
                        while (stack.Any() && (stack.Peek().Type != TokenType.Comma &&
                                               stack.Peek().Type != TokenType.Parenthesis))
                            yield return stack.Pop();
                        break;
                    case TokenType.Parenthesis:
                        if (tok.Value == OpenFuncStr)
                        {
                            if (stack.Any() && stack.Peek().Type == TokenType.Function)
                            {
                                if (includeFunctionStopper)
                                    yield return new Token(TokenType.Wall, tok.Value);
                            }
                            stack.Push(tok);
                        }
                        else
                        {
                            while (stack.Peek().Value != OpenFuncStr)
                                yield return stack.Pop();
                            stack.Pop();
                            if (stack.Any() && stack.Peek().Type == TokenType.Function)
                            {
                                yield return stack.Pop();
                            }
                        }
                        break;
                    default:
                        throw new InvalidOperationException("Wrong token");
                }
            }
            while (stack.Any())
            {
                var tok = stack.Pop();
                if (tok.Type == TokenType.Parenthesis)
                    throw new InvalidOperationException("Mismatched parenthesis");
                yield return tok;
            }
        }
        private static bool CompareOperators(Operator op1, Operator op2) => op1.RightAssociative
            ? op1.Precedence < op2.Precedence
            : op1.Precedence <= op2.Precedence;
        private void Tokenize(string input)
        {
            if (!ValidateInput(input, out var startIndex))
            {
                return;
            }
            for (var i = startIndex; i < input.Length; i++)
            {
                if (char.IsWhiteSpace(input, i)) continue;
                if (input[i] == CommaChar)
                {
                    Tokens.Add(new Token(TokenType.Comma, new string(new[] { input[i] })));
                    continue;
                }
                if (input[i] == StringQuotedChar)
                {
                    i = ExtractString(input, i);
                    continue;
                }
                if (char.IsLetter(input, i) || EvaluateFunctionOrMember(input, i))
                {
                    i = ExtractFunctionOrMember(input, i);
                    continue;
                }
                if (char.IsNumber(input, i) || (
                        input[i] == NegativeChar &&
                        ((Tokens.Any() && Tokens.Last().Type != TokenType.Number) || !Tokens.Any())))
                {
                    i = ExtractNumber(input, i);
                    continue;
                }
                if (input[i] == OpenFuncChar ||
                    input[i] == CloseFuncChar)
                {
                    Tokens.Add(new Token(TokenType.Parenthesis, new string(new[] { input[i] })));
                    continue;
                }
                i = ExtractOperator(input, i);
            }
        }
        private int ExtractData(
            string input,
            int i,
            Func tokenTypeEvaluation,
            Func evaluation,
            int right = 0,
            int left = -1)
        {
            var charCount = 0;
            for (var j = i + right; j < input.Length; j++)
            {
                if (evaluation(input[j]))
                    break;
                charCount++;
            }
            // Extract and set the value
            var value = input.SliceLength(i + right, charCount);
            Tokens.Add(new Token(tokenTypeEvaluation(value), value));
            i += charCount + left;
            return i;
        }
        private int ExtractOperator(string input, int i) =>
            ExtractData(
                input,
                i,
                x => TokenType.Operator,
                x => x == OpenFuncChar ||
                                                                x == CommaChar ||
                                                                x == PeriodChar ||
                                                                x == StringQuotedChar ||
                                                                char.IsWhiteSpace(x) ||
                                                                char.IsNumber(x));
        private int ExtractFunctionOrMember(string input, int i) =>
            ExtractData(
                input,
                i,
                ResolveFunctionOrMemberType,
                x => x == OpenFuncChar ||
                                                                    x == CloseFuncChar ||
                                                                    x == CommaChar ||
                                                                    char.IsWhiteSpace(x));
        private int ExtractNumber(string input, int i) =>
            ExtractData(
                input,
                i,
                x => TokenType.Number,
                x => !char.IsNumber(x) && x != PeriodChar && x != NegativeChar);
        private int ExtractString(string input, int i)
        {
            var length = ExtractData(input, i, x => TokenType.String, x => x == StringQuotedChar, 1, 1);
            // open string, report issue
            if (length == input.Length && input[length - 1] != StringQuotedChar)
                throw new FormatException($"Parser error (Position {i}): Expected '\"' but got '{input[length - 1]}'.");
            return length;
        }
        private bool CompareOperators(string op1, string op2)
            => CompareOperators(GetOperatorOrDefault(op1), GetOperatorOrDefault(op2));
        private Operator GetOperatorOrDefault(string op)
            => _operators.FirstOrDefault(x => x.Name == op) ?? new Operator { Name = op, Precedence = 0 };
    }
}