using Lucene.Net.QueryParsers.Flexible.Core.Messages;
using Lucene.Net.QueryParsers.Flexible.Core.Parser;
using Lucene.Net.QueryParsers.Flexible.Messages;
using System;
using System.Collections.Generic;
using System.Text;

namespace Lucene.Net.QueryParsers.Flexible.Core.Nodes
{
    /*
     * 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.
     */

    /// <summary>
    /// A <see cref="ProximityQueryNode"/> represents a query where the terms should meet
    /// specific distance conditions. (a b c) WITHIN [SENTENCE|PARAGRAPH|NUMBER]
    /// [INORDER] ("a" "b" "c") WITHIN [SENTENCE|PARAGRAPH|NUMBER] [INORDER]
    /// 
    /// TODO: Add this to the future standard Lucene parser/processor/builder
    /// </summary>
    public class ProximityQueryNode : BooleanQueryNode
    {
        /// <summary>
        /// Distance condition: PARAGRAPH, SENTENCE, or NUMBER
        /// </summary>
        public enum Type
        {
            PARAGRAPH,
            SENTENCE,
            NUMBER
        }

        // LUCENENET NOTE: Moved ProximityType class outside of ProximityQueryNode class to
        // prevent a naming conflict with the ProximityType property.

        private ProximityQueryNode.Type proximityType = ProximityQueryNode.Type.SENTENCE;
        private int distance = -1;
        private bool inorder = false;
        private string field = null;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="clauses">QueryNode children</param>
        /// <param name="field">field name</param>
        /// <param name="type">type of proximity query</param>
        /// <param name="distance">positive integer that specifies the distance</param>
        /// <param name="inorder">true, if the tokens should be matched in the order of the clauses</param>
        public ProximityQueryNode(IList<IQueryNode> clauses, string field,
            ProximityQueryNode.Type type, int distance, bool inorder)
            : base(clauses)
        {
            IsLeaf = false;
            this.proximityType = type;
            this.inorder = inorder;
            this.field = field;
            if (type == ProximityQueryNode.Type.NUMBER)
            {
                if (distance <= 0)
                {
                    throw new QueryNodeError(new Message(
                        QueryParserMessages.PARAMETER_VALUE_NOT_SUPPORTED, "distance",
                        distance));
                }
                else
                {
                    this.distance = distance;
                }
            }
            ClearFields(clauses, field);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="clauses">QueryNode children</param>
        /// <param name="field">field name</param>
        /// <param name="type">type of proximity query</param>
        /// <param name="inorder">true, if the tokens should be matched in the order of the clauses</param>
        public ProximityQueryNode(IList<IQueryNode> clauses, string field,
            ProximityQueryNode.Type type, bool inorder)
            : this(clauses, field, type, -1, inorder)
        {
        }

        private static void ClearFields(IList<IQueryNode> nodes, string field)
        {
            if (nodes == null || nodes.Count == 0)
                return;

            foreach (IQueryNode clause in nodes)
            {
                if (clause is FieldQueryNode)
                {
                    ((FieldQueryNode)clause).m_toQueryStringIgnoreFields = true;
                    ((FieldQueryNode)clause).Field = field;
                }
            }
        }

        public virtual ProximityQueryNode.Type ProximityType => this.proximityType;

        public override string ToString()
        {
            string distanceSTR = ((this.distance == -1) ? ("")
                : (" distance='" + this.distance) + "'");

            var children = GetChildren();
            if (children == null || children.Count == 0)
                return "<proximity field='" + this.field + "' inorder='" + this.inorder
                    + "' type='" + this.proximityType.ToString() + "'" + distanceSTR
                    + "/>";
            StringBuilder sb = new StringBuilder();
            sb.Append("<proximity field='" + this.field + "' inorder='" + this.inorder
                + "' type='" + this.proximityType.ToString() + "'" + distanceSTR + ">");
            foreach (IQueryNode child in children)
            {
                sb.Append("\n");
                sb.Append(child.ToString());
            }
            sb.Append("\n</proximity>");
            return sb.ToString();
        }

        public override string ToQueryString(IEscapeQuerySyntax escapeSyntaxParser)
        {
            string withinSTR = this.proximityType.ToQueryString()
                + ((this.distance == -1) ? ("") : (" " + this.distance))
                + ((this.inorder) ? (" INORDER") : (""));

            StringBuilder sb = new StringBuilder();
            var children = GetChildren();
            if (children == null || children.Count == 0)
            {
                // no children case
            }
            else
            {
                string filler = "";
                foreach (IQueryNode child in children)
                {
                    sb.Append(filler).Append(child.ToQueryString(escapeSyntaxParser));
                    filler = " ";
                }
            }

            if (IsDefaultField(this.field))
            {
                return "( " + sb.ToString() + " ) " + withinSTR;
            }
            else
            {
                return this.field + ":(( " + sb.ToString() + " ) " + withinSTR + ")";
            }
        }

        public override IQueryNode CloneTree()
        {
            ProximityQueryNode clone = (ProximityQueryNode)base.CloneTree();

            clone.proximityType = this.proximityType;
            clone.distance = this.distance;
            clone.field = this.field;

            return clone;
        }

        /// <summary>
        /// Gets the distance
        /// </summary>
        public virtual int Distance => this.distance;

        /// <summary>
        /// Gets or Sets the field. Returns null if the field was not specified in the query string.
        /// </summary>
        public virtual string Field
        {
            get => this.field;
            set => this.field = value;
        }

        // LUCENENET specific: This method is technically not required because Field is already a string property, not ICharSequence
        /// <summary>
        /// Gets the field as a string. Returns null if the field was not specified in the query string.
        /// </summary>
        /// <returns></returns>
        public virtual string GetFieldAsString()
        {
            if (this.field == null)
                return null;
            else
                return this.field.ToString();
        }

        /// <summary>
        /// terms must be matched in the specified order
        /// </summary>
        public virtual bool IsInOrder => this.inorder;
    }

    /// <summary>
    /// utility class containing the distance condition and number
    /// </summary>
    public class ProximityType
    {
        internal int pDistance = 0;

        ProximityQueryNode.Type pType/* = null*/;

        public ProximityType(ProximityQueryNode.Type type)
                : this(type, 0)
        {
        }

        public ProximityType(ProximityQueryNode.Type type, int distance)
        {
            this.pType = type;
            this.pDistance = distance;
        }
    }

    public static class ProximityQueryNode_TypeExtensions
    {
        public static string ToQueryString(this ProximityQueryNode.Type type)
        {
            switch (type)
            {
                case ProximityQueryNode.Type.NUMBER:
                    return "WITHIN";
                case ProximityQueryNode.Type.PARAGRAPH:
                    return "WITHIN PARAGRAPH";
                case ProximityQueryNode.Type.SENTENCE:
                    return "WITHIN SENTENCE";
            }

            throw new ArgumentException("Invalid ProximityQueryNode.Type");
        }
    }
}
