using Lucene.Net.Analysis;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Util;
using Lucene.Net.Util.Automaton;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JCG = J2N.Collections.Generic;

namespace Lucene.Net.QueryParsers.Simple
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    // LUCENENET specific - converted constants from SimpleQueryParser
    // into a flags enum.
    [Flags]
    public enum Operator
    {
        /// <summary>Enables <c>AND</c> operator (+)</summary>
        AND_OPERATOR = 1 << 0,
        /// <summary>Enables <c>NOT</c> operator (-)</summary>
        NOT_OPERATOR = 1 << 1,
        /// <summary>Enables <c>OR</c> operator (|)</summary>
        OR_OPERATOR = 1 << 2,
        /// <summary>Enables <c>PREFIX</c> operator (*)</summary>
        PREFIX_OPERATOR = 1 << 3,
        /// <summary>Enables <c>PHRASE</c> operator (")</summary>
        PHRASE_OPERATOR = 1 << 4,
        /// <summary>Enables <c>PRECEDENCE</c> operators: <c>(</c> and <c>)</c></summary>
        PRECEDENCE_OPERATORS = 1 << 5,
        /// <summary>Enables <c>ESCAPE</c> operator (\)</summary>
        ESCAPE_OPERATOR = 1 << 6,
        /// <summary>Enables <c>WHITESPACE</c> operators: ' ' '\n' '\r' '\t'</summary>
        WHITESPACE_OPERATOR = 1 << 7,
        /// <summary>Enables <c>FUZZY</c> operators: (~) on single terms</summary>
        FUZZY_OPERATOR = 1 << 8,
        /// <summary>Enables <c>NEAR</c> operators: (~) on phrases</summary>
        NEAR_OPERATOR = 1 << 9
    }

    /// <summary>
    /// <see cref="SimpleQueryParser"/> is used to parse human readable query syntax.
    /// <para/>
    /// The main idea behind this parser is that a person should be able to type
    /// whatever they want to represent a query, and this parser will do its best
    /// to interpret what to search for no matter how poorly composed the request
    /// may be. Tokens are considered to be any of a term, phrase, or subquery for the
    /// operations described below.  Whitespace including ' ' '\n' '\r' and '\t'
    /// and certain operators may be used to delimit tokens ( ) + | " .
    /// <para/>
    /// Any errors in query syntax will be ignored and the parser will attempt
    /// to decipher what it can; however, this may mean odd or unexpected results.
    /// <h4>Query Operators</h4>
    /// <list type="bullet">
    ///  <item><description>'<c>+</c>' specifies <c>AND</c> operation: <c>token1+token2</c></description></item>
    ///  <item><description>'<c>|</c>' specifies <c>OR</c> operation: <c>token1|token2</c></description></item>
    ///  <item><description>'<c>-</c>' negates a single token: <c>-token0</c></description></item>
    ///  <item><description>'<c>"</c>' creates phrases of terms: <c>"term1 term2 ..."</c></description></item>
    ///  <item><description>'<c>*</c>' at the end of terms specifies prefix query: <c>term*</c></description></item>
    ///  <item><description>'<c>~</c>N' at the end of terms specifies fuzzy query: <c>term~1</c></description></item>
    ///  <item><description>'<c>~</c>N' at the end of phrases specifies near query: <c>"term1 term2"~5</c></description></item>
    ///  <item><description>'<c>(</c>' and '<c>)</c>' specifies precedence: <c>token1 + (token2 | token3)</c></description></item>
    /// </list>
    /// <para/>
    /// The default operator is <c>OR</c> if no other operator is specified.
    /// For example, the following will <c>OR</c> <c>token1</c> and <c>token2</c> together:
    /// <c>token1 token2</c>
    /// <para/>
    /// Normal operator precedence will be simple order from right to left.
    /// For example, the following will evaluate <c>token1 OR token2</c> first,
    /// then <c>AND</c> with <c>token3</c>:
    /// <code>token1 | token2 + token3</code>
    /// <h4>Escaping</h4>
    /// <para/>
    /// An individual term may contain any possible character with certain characters
    /// requiring escaping using a '<c>\</c>'.  The following characters will need to be escaped in
    /// terms and phrases:
    /// <c>+ | " ( ) ' \</c>
    /// <para/>
    /// The '<c>-</c>' operator is a special case.  On individual terms (not phrases) the first
    /// character of a term that is <c>-</c> must be escaped; however, any '<c>-</c>' characters
    /// beyond the first character do not need to be escaped.
    /// For example:
    /// <list type="bullet">
    ///   <item><description><c>-term1</c>   -- Specifies <c>NOT</c> operation against <c>term1</c></description></item>
    ///   <item><description><c>\-term1</c>  -- Searches for the term <c>-term1</c>.</description></item>
    ///   <item><description><c>term-1</c>   -- Searches for the term <c>term-1</c>.</description></item>
    ///   <item><description><c>term\-1</c>  -- Searches for the term <c>term-1</c>.</description></item>
    /// </list>
    /// <para/>
    /// The '<c>*</c>' operator is a special case. On individual terms (not phrases) the last
    /// character of a term that is '<c>*</c>' must be escaped; however, any '<c>*</c>' characters
    /// before the last character do not need to be escaped:
    /// <list type="bullet">
    ///   <item><description><c>term1*</c>  --  Searches for the prefix <c>term1</c></description></item>
    ///   <item><description><c>term1\*</c> --  Searches for the term <c>term1*</c></description></item>
    ///   <item><description><c>term*1</c>  --  Searches for the term <c>term*1</c></description></item>
    ///   <item><description><c>term\*1</c> --  Searches for the term <c>term*1</c></description></item>
    /// </list>
    /// <para/>
    /// Note that above examples consider the terms before text processing.
    /// </summary>
    public class SimpleQueryParser : QueryBuilder
    {
        /// <summary>Map of fields to query against with their weights</summary>
        protected readonly IDictionary<string, float> m_weights;

        // LUCENENET specific - made flags into their own [Flags] enum named Operator and de-nested from this type

        /// <summary>flags to the parser (to turn features on/off)</summary>
        protected readonly Operator m_flags;

        private Occur defaultOperator = Occur.SHOULD;

        /// <summary>Creates a new parser searching over a single field.</summary>
        public SimpleQueryParser(Analyzer analyzer, string field)
            : this(analyzer, new JCG.Dictionary<string, float>() { { field, 1.0F } })
        {
        }

        /// <summary>Creates a new parser searching over multiple fields with different weights.</summary>
        public SimpleQueryParser(Analyzer analyzer, IDictionary<string, float> weights)
            : this(analyzer, weights, (Operator)(-1))
        {
        }

        /// <summary>Creates a new parser with custom flags used to enable/disable certain features.</summary>
        public SimpleQueryParser(Analyzer analyzer, IDictionary<string, float> weights, Operator flags)
            : base(analyzer)
        {
            this.m_weights = weights;
            this.m_flags = flags;
        }

        /// <summary>Parses the query text and returns parsed query (or null if empty)</summary>
        public Query Parse(string queryText)
        {
            char[] data = queryText.ToCharArray();
            char[] buffer = new char[data.Length];

            State state = new State(data, buffer, 0, data.Length);
            ParseSubQuery(state);
            return state.Top;
        }

        private void ParseSubQuery(State state)
        {
            while (state.Index < state.Length)
            {
                if (state.Data[state.Index] == '(' && (m_flags & Operator.PRECEDENCE_OPERATORS) != 0)
                {
                    // the beginning of a subquery has been found
                    ConsumeSubQuery(state);
                }
                else if (state.Data[state.Index] == ')' && (m_flags & Operator.PRECEDENCE_OPERATORS) != 0)
                {
                    // this is an extraneous character so it is ignored
                    ++state.Index;
                }
                else if (state.Data[state.Index] == '"' && (m_flags & Operator.PHRASE_OPERATOR) != 0)
                {
                    // the beginning of a phrase has been found
                    ConsumePhrase(state);
                }
                else if (state.Data[state.Index] == '+' && (m_flags & Operator.AND_OPERATOR) != 0)
                {
                    // an and operation has been explicitly set
                    // if an operation has already been set this one is ignored
                    // if a term (or phrase or subquery) has not been found yet the
                    // operation is also ignored since there is no previous
                    // term (or phrase or subquery) to and with
                    if (!state.CurrentOperationIsSet && state.Top != null)
                    {
                        state.CurrentOperation = Occur.MUST;
                    }

                    ++state.Index;
                }
                else if (state.Data[state.Index] == '|' && (m_flags & Operator.OR_OPERATOR) != 0)
                {
                    // an or operation has been explicitly set
                    // if an operation has already been set this one is ignored
                    // if a term (or phrase or subquery) has not been found yet the
                    // operation is also ignored since there is no previous
                    // term (or phrase or subquery) to or with
                    if (!state.CurrentOperationIsSet && state.Top != null)
                    {
                        state.CurrentOperation = Occur.SHOULD;
                    }

                    ++state.Index;
                }
                else if (state.Data[state.Index] == '-' && (m_flags & Operator.NOT_OPERATOR) != 0)
                {
                    // a not operator has been found, so increase the not count
                    // two not operators in a row negate each other
                    ++state.Not;
                    ++state.Index;

                    // continue so the not operator is not reset
                    // before the next character is determined
                    continue;
                }
                else if ((state.Data[state.Index] == ' '
                  || state.Data[state.Index] == '\t'
                  || state.Data[state.Index] == '\n'
                  || state.Data[state.Index] == '\r') && (m_flags & Operator.WHITESPACE_OPERATOR) != 0)
                {
                    // ignore any whitespace found as it may have already been
                    // used a delimiter across a term (or phrase or subquery)
                    // or is simply extraneous
                    ++state.Index;
                }
                else
                {
                    // the beginning of a token has been found
                    ConsumeToken(state);
                }

                // reset the not operator as even whitespace is not allowed when
                // specifying the not operation for a term (or phrase or subquery)
                state.Not = 0;
            }
        }

        private void ConsumeSubQuery(State state)
        {
            Debug.Assert((m_flags & Operator.PRECEDENCE_OPERATORS) != 0);
            int start = ++state.Index;
            int precedence = 1;
            bool escaped = false;

            while (state.Index < state.Length)
            {
                if (!escaped)
                {
                    if (state.Data[state.Index] == '\\' && (m_flags & Operator.ESCAPE_OPERATOR) != 0)
                    {
                        // an escape character has been found so
                        // whatever character is next will become
                        // part of the subquery unless the escape
                        // character is the last one in the data
                        escaped = true;
                        ++state.Index;

                        continue;
                    }
                    else if (state.Data[state.Index] == '(')
                    {
                        // increase the precedence as there is a
                        // subquery in the current subquery
                        ++precedence;
                    }
                    else if (state.Data[state.Index] == ')')
                    {
                        --precedence;

                        if (precedence == 0)
                        {
                            // this should be the end of the subquery
                            // all characters found will used for
                            // creating the subquery
                            break;
                        }
                    }
                }

                escaped = false;
                ++state.Index;
            }

            if (state.Index == state.Length)
            {
                // a closing parenthesis was never found so the opening
                // parenthesis is considered extraneous and will be ignored
                state.Index = start;
            }
            else if (state.Index == start)
            {
                // a closing parenthesis was found immediately after the opening
                // parenthesis so the current operation is reset since it would
                // have been applied to this subquery
                state.CurrentOperationIsSet = false;

                ++state.Index;
            }
            else
            {
                // a complete subquery has been found and is recursively parsed by
                // starting over with a new state object
                State subState = new State(state.Data, state.Buffer, start, state.Index);
                ParseSubQuery(subState);
                BuildQueryTree(state, subState.Top);

                ++state.Index;
            }
        }

        private void ConsumePhrase(State state)
        {
            Debug.Assert((m_flags & Operator.PHRASE_OPERATOR) != 0);
            int start = ++state.Index;
            int copied = 0;
            bool escaped = false;
            bool hasSlop = false;

            while (state.Index < state.Length)
            {
                if (!escaped)
                {
                    if (state.Data[state.Index] == '\\' && (m_flags & Operator.ESCAPE_OPERATOR) != 0)
                    {
                        // an escape character has been found so
                        // whatever character is next will become
                        // part of the phrase unless the escape
                        // character is the last one in the data
                        escaped = true;
                        ++state.Index;

                        continue;
                    }
                    else if (state.Data[state.Index] == '"')
                    {
                        // if there are still characters after the closing ", check for a
                        // tilde
                        if (state.Length > (state.Index + 1) &&
                            state.Data[state.Index + 1] == '~' &&
                            (m_flags & Operator.NEAR_OPERATOR) != 0)
                        {
                            state.Index++;
                            // check for characters after the tilde
                            if (state.Length > (state.Index + 1))
                            {
                                hasSlop = true;
                            }
                            break;
                        }
                        else
                        {
                            // this should be the end of the phrase
                            // all characters found will used for
                            // creating the phrase query
                            break;
                        }
                    }
                }

                escaped = false;
                state.Buffer[copied++] = state.Data[state.Index++];
            }

            if (state.Index == state.Length)
            {
                // a closing double quote was never found so the opening
                // double quote is considered extraneous and will be ignored
                state.Index = start;
            }
            else if (state.Index == start)
            {
                // a closing double quote was found immediately after the opening
                // double quote so the current operation is reset since it would
                // have been applied to this phrase
                state.CurrentOperationIsSet = false;

                ++state.Index;
            }
            else
            {
                // a complete phrase has been found and is parsed through
                // through the analyzer from the given field
                string phrase = new string(state.Buffer, 0, copied);
                Query branch;
                if (hasSlop)
                {
                    branch = NewPhraseQuery(phrase, ParseFuzziness(state));
                }
                else
                {
                    branch = NewPhraseQuery(phrase, 0);
                }
                BuildQueryTree(state, branch);

                ++state.Index;
            }
        }

        private void ConsumeToken(State state)
        {
            int copied = 0;
            bool escaped = false;
            bool prefix = false;
            bool fuzzy = false;

            while (state.Index < state.Length)
            {
                if (!escaped)
                {
                    if (state.Data[state.Index] == '\\' && (m_flags & Operator.ESCAPE_OPERATOR) != 0)
                    {
                        // an escape character has been found so
                        // whatever character is next will become
                        // part of the term unless the escape
                        // character is the last one in the data
                        escaped = true;
                        prefix = false;
                        ++state.Index;

                        continue;
                    }
                    else if (TokenFinished(state))
                    {
                        // this should be the end of the term
                        // all characters found will used for
                        // creating the term query
                        break;
                    }
                    else if (copied > 0 && state.Data[state.Index] == '~' && (m_flags & Operator.FUZZY_OPERATOR) != 0)
                    {
                        fuzzy = true;
                        break;
                    }

                    // wildcard tracks whether or not the last character
                    // was a '*' operator that hasn't been escaped
                    // there must be at least one valid character before
                    // searching for a prefixed set of terms
                    prefix = copied > 0 && state.Data[state.Index] == '*' && (m_flags & Operator.PREFIX_OPERATOR) != 0;
                }

                escaped = false;
                state.Buffer[copied++] = state.Data[state.Index++];
            }

            if (copied > 0)
            {
                Query branch;

                if (fuzzy && (m_flags & Operator.FUZZY_OPERATOR) != 0)
                {
                    string token = new string(state.Buffer, 0, copied);
                    int fuzziness = ParseFuzziness(state);
                    // edit distance has a maximum, limit to the maximum supported
                    fuzziness = Math.Min(fuzziness, LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE);
                    if (fuzziness == 0)
                    {
                        branch = NewDefaultQuery(token);
                    }
                    else
                    {
                        branch = NewFuzzyQuery(token, fuzziness);
                    }
                }
                else if (prefix)
                {
                    // if a term is found with a closing '*' it is considered to be a prefix query
                    // and will have prefix added as an option
                    string token = new string(state.Buffer, 0, copied - 1);
                    branch = NewPrefixQuery(token);
                }
                else
                {
                    // a standard term has been found so it will be run through
                    // the entire analysis chain from the specified schema field
                    string token = new string(state.Buffer, 0, copied);
                    branch = NewDefaultQuery(token);
                }

                BuildQueryTree(state, branch);
            }
        }

        /// <summary>
        /// buildQueryTree should be called after a term, phrase, or subquery
        /// is consumed to be added to our existing query tree
        /// this method will only add to the existing tree if the branch contained in state is not null
        /// </summary>
        private void BuildQueryTree(State state, Query branch)
        {
            if (branch != null)
            {
                // modify our branch to a BooleanQuery wrapper for not
                // this is necessary any time a term, phrase, or subquery is negated
                if (state.Not % 2 == 1)
                {
                    BooleanQuery nq = new BooleanQuery();
                    nq.Add(branch, Occur.MUST_NOT);
                    nq.Add(new MatchAllDocsQuery(), Occur.SHOULD);
                    branch = nq;
                }

                // first term (or phrase or subquery) found and will begin our query tree
                if (state.Top == null)
                {
                    state.Top = branch;
                }
                else
                {
                    // more than one term (or phrase or subquery) found
                    // set currentOperation to the default if no other operation is explicitly set
                    if (!state.CurrentOperationIsSet)
                    {
                        state.CurrentOperation = defaultOperator;
                    }

                    // operational change requiring a new parent node
                    // this occurs if the previous operation is not the same as current operation
                    // because the previous operation must be evaluated separately to preserve
                    // the proper precedence and the current operation will take over as the top of the tree
                    if (!state.PreviousOperationIsSet || state.PreviousOperation != state.CurrentOperation)
                    {
                        BooleanQuery bq = new BooleanQuery();
                        bq.Add(state.Top, state.CurrentOperation);
                        state.Top = bq;
                    }

                    // reset all of the state for reuse
                    ((BooleanQuery)state.Top).Add(branch, state.CurrentOperation);
                    state.PreviousOperation = state.CurrentOperation;
                }

                // reset the current operation as it was intended to be applied to
                // the incoming term (or phrase or subquery) even if branch was null
                // due to other possible errors
                state.CurrentOperationIsSet = false;
            }
        }

        /// <summary>
        /// Helper parsing fuzziness from parsing state
        /// </summary>
        /// <returns>slop/edit distance, 0 in the case of non-parsing slop/edit string</returns>
        private int ParseFuzziness(State state)
        {
            char[] slopText = new char[state.Length];
            int slopLength = 0;

            if (state.Data[state.Index] == '~')
            {
                while (state.Index < state.Length)
                {
                    state.Index++;
                    // it's possible that the ~ was at the end, so check after incrementing
                    // to make sure we don't go out of bounds
                    if (state.Index < state.Length)
                    {
                        if (TokenFinished(state))
                        {
                            break;
                        }
                        slopText[slopLength] = state.Data[state.Index];
                        slopLength++;
                    }
                }
                int fuzziness = 0;
                int.TryParse(new string(slopText, 0, slopLength), out fuzziness); // LUCENENET TODO: Find a way to pass culture
                // negative -> 0
                if (fuzziness < 0)
                {
                    fuzziness = 0;
                }
                return fuzziness;
            }
            return 0;
        }

        /// <summary>
        /// Helper returning true if the state has reached the end of token.
        /// </summary>
        private bool TokenFinished(State state)
        {
            if ((state.Data[state.Index] == '"' && (m_flags & Operator.PHRASE_OPERATOR) != 0)
                || (state.Data[state.Index] == '|' && (m_flags & Operator.OR_OPERATOR) != 0)
                || (state.Data[state.Index] == '+' && (m_flags & Operator.AND_OPERATOR) != 0)
                || (state.Data[state.Index] == '(' && (m_flags & Operator.PRECEDENCE_OPERATORS) != 0)
                || (state.Data[state.Index] == ')' && (m_flags & Operator.PRECEDENCE_OPERATORS) != 0)
                || ((state.Data[state.Index] == ' '
                || state.Data[state.Index] == '\t'
                || state.Data[state.Index] == '\n'
                || state.Data[state.Index] == '\r') && (m_flags & Operator.WHITESPACE_OPERATOR) != 0))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Factory method to generate a standard query (no phrase or prefix operators).
        /// </summary>
        protected virtual Query NewDefaultQuery(string text)
        {
            BooleanQuery bq = new BooleanQuery(true);
            foreach (var entry in m_weights)
            {
                Query q = CreateBooleanQuery(entry.Key, text, defaultOperator);
                if (q != null)
                {
                    q.Boost = entry.Value;
                    bq.Add(q, Occur.SHOULD);
                }
            }
            return Simplify(bq);
        }

        /// <summary>
        /// Factory method to generate a fuzzy query.
        /// </summary>
        protected virtual Query NewFuzzyQuery(string text, int fuzziness)
        {
            BooleanQuery bq = new BooleanQuery(true);
            foreach (var entry in m_weights)
            {
                Query q = new FuzzyQuery(new Term(entry.Key, text), fuzziness);
                if (q != null)
                {
                    q.Boost = entry.Value;
                    bq.Add(q, Occur.SHOULD);
                }
            }
            return Simplify(bq);
        }

        /// <summary>
        /// Factory method to generate a phrase query with slop.
        /// </summary>
        protected virtual Query NewPhraseQuery(string text, int slop)
        {
            BooleanQuery bq = new BooleanQuery(true);
            foreach (var entry in m_weights)
            {
                Query q = CreatePhraseQuery(entry.Key, text, slop);
                if (q != null)
                {
                    q.Boost = entry.Value;
                    bq.Add(q, Occur.SHOULD);
                }
            }
            return Simplify(bq);
        }

        /// <summary>
        /// Factory method to generate a prefix query.
        /// </summary>
        protected virtual Query NewPrefixQuery(string text)
        {
            BooleanQuery bq = new BooleanQuery(true);
            foreach (var entry in m_weights)
            {
                PrefixQuery prefix = new PrefixQuery(new Term(entry.Key, text));
                prefix.Boost = entry.Value;
                bq.Add(prefix, Occur.SHOULD);
            }
            return Simplify(bq);
        }

        /// <summary>
        /// Helper to simplify boolean queries with 0 or 1 clause
        /// </summary>
        protected virtual Query Simplify(BooleanQuery bq)
        {
            if (bq.Clauses.Count == 0)
            {
                return null;
            }
            else if (bq.Clauses.Count == 1)
            {
                return bq.Clauses[0].Query;
            }
            else
            {
                return bq;
            }
        }

        /// <summary>
        /// Gets or Sets the implicit operator setting, which will be
        /// either <see cref="Occur.SHOULD"/> or <see cref="Occur.MUST"/>.
        /// </summary>
        public virtual Occur DefaultOperator
        {
            get => defaultOperator;
            set 
            {
                if (value != Occur.SHOULD && value != Occur.MUST)
                {
                    throw new ArgumentException("invalid operator: only SHOULD or MUST are allowed");
                }
                defaultOperator = value; 
            }
        }


        internal class State
        {
            //private readonly char[] data;   // the characters in the query string
            //private readonly char[] buffer; // a temporary buffer used to reduce necessary allocations
            //private int index;
            //private int length;

            private Occur currentOperation;
            private Occur previousOperation;
            //private int not;

            //private Query top;

            internal State(char[] data, char[] buffer, int index, int length)
            {
                this.Data = data;
                this.Buffer = buffer;
                this.Index = index;
                this.Length = length;
            }

            internal char[] Data { get; private set; } // the characters in the query string
            internal char[] Buffer { get; private set; } // a temporary buffer used to reduce necessary allocations
            internal int Index { get; set; }
            internal int Length { get; set; }

            internal Occur CurrentOperation 
            {
                get => currentOperation;
                set
                {
                    currentOperation = value;
                    CurrentOperationIsSet = true;
                }
            }

            internal Occur PreviousOperation
            {
                get => previousOperation;
                set
                {
                    previousOperation = value;
                    PreviousOperationIsSet = true;
                }
            }

            internal bool CurrentOperationIsSet { get; set; }
            internal bool PreviousOperationIsSet { get; set; }

            internal int Not { get; set; }
            internal Query Top { get; set; }
        }
    }
}
