using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JCG = J2N.Collections.Generic;

namespace Lucene.Net.Search.Spans
{
    /*
     * 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.
     */

    using AtomicReaderContext = Lucene.Net.Index.AtomicReaderContext;
    using IBits = Lucene.Net.Util.IBits;
    using IndexReader = Lucene.Net.Index.IndexReader;
    using Term = Lucene.Net.Index.Term;
    using TermContext = Lucene.Net.Index.TermContext;
    using ToStringUtils = Lucene.Net.Util.ToStringUtils;

    /// <summary>
    /// Matches spans which are near one another.  One can specify <i>slop</i>, the
    /// maximum number of intervening unmatched positions, as well as whether
    /// matches are required to be in-order.
    /// </summary>
    public class SpanNearQuery : SpanQuery
#if FEATURE_CLONEABLE
        , System.ICloneable
#endif
    {
        protected readonly IList<SpanQuery> m_clauses;
        protected int m_slop;
        protected bool m_inOrder;

        protected string m_field;
        private bool collectPayloads;

        /// <summary>
        /// Construct a <see cref="SpanNearQuery"/>.  Matches spans matching a span from each
        /// clause, with up to <paramref name="slop"/> total unmatched positions between
        /// them.  * When <paramref name="inOrder"/> is <c>true</c>, the spans from each clause
        /// must be * ordered as in <paramref name="clauses"/>. </summary>
        /// <param name="clauses"> The clauses to find near each other </param>
        /// <param name="slop"> The slop value </param>
        /// <param name="inOrder"> <c>true</c> if order is important</param>
        public SpanNearQuery(SpanQuery[] clauses, int slop, bool inOrder)
            : this(clauses, slop, inOrder, true)
        {
        }

        public SpanNearQuery(SpanQuery[] clauses, int slop, bool inOrder, bool collectPayloads)
        {
            // copy clauses array into an ArrayList
            this.m_clauses = new List<SpanQuery>(clauses.Length);
            for (int i = 0; i < clauses.Length; i++)
            {
                SpanQuery clause = clauses[i];
                if (m_field == null) // check field
                {
                    m_field = clause.Field;
                }
                else if (clause.Field != null && !clause.Field.Equals(m_field, StringComparison.Ordinal))
                {
                    throw new System.ArgumentException("Clauses must have same field.");
                }
                this.m_clauses.Add(clause);
            }
            this.collectPayloads = collectPayloads;
            this.m_slop = slop;
            this.m_inOrder = inOrder;
        }

        /// <summary>
        /// Return the clauses whose spans are matched. </summary>
        public virtual SpanQuery[] GetClauses()
        {
            return m_clauses.ToArray();
        }

        /// <summary>
        /// Return the maximum number of intervening unmatched positions permitted. </summary>
        public virtual int Slop => m_slop;

        /// <summary>
        /// Return <c>true</c> if matches are required to be in-order. </summary>
        public virtual bool IsInOrder => m_inOrder;

        public override string Field => m_field;

        public override void ExtractTerms(ISet<Term> terms)
        {
            foreach (SpanQuery clause in m_clauses)
            {
                clause.ExtractTerms(terms);
            }
        }

        public override string ToString(string field)
        {
            StringBuilder buffer = new StringBuilder();
            buffer.Append("SpanNear([");
            bool isFirst = true;
            foreach (SpanQuery clause in m_clauses)
            {
                if (!isFirst)
                {
                    buffer.Append(", ");
                }
                buffer.Append(clause.ToString(field));
                isFirst = false;
            }
            buffer.Append("], ");
            buffer.Append(m_slop);
            buffer.Append(", ");
            buffer.Append(m_inOrder);
            buffer.Append(")");
            buffer.Append(ToStringUtils.Boost(Boost));
            return buffer.ToString();
        }

        public override Spans GetSpans(AtomicReaderContext context, IBits acceptDocs, IDictionary<Term, TermContext> termContexts)
        {
            if (m_clauses.Count == 0) // optimize 0-clause case
            {
                return (new SpanOrQuery(GetClauses())).GetSpans(context, acceptDocs, termContexts);
            }

            if (m_clauses.Count == 1) // optimize 1-clause case
            {
                return m_clauses[0].GetSpans(context, acceptDocs, termContexts);
            }

            return m_inOrder ? (Spans)new NearSpansOrdered(this, context, acceptDocs, termContexts, collectPayloads) : (Spans)new NearSpansUnordered(this, context, acceptDocs, termContexts);
        }

        public override Query Rewrite(IndexReader reader)
        {
            SpanNearQuery clone = null;
            for (int i = 0; i < m_clauses.Count; i++)
            {
                SpanQuery c = m_clauses[i];
                SpanQuery query = (SpanQuery)c.Rewrite(reader);
                if (query != c) // clause rewrote: must clone
                {
                    if (clone == null)
                    {
                        clone = (SpanNearQuery)this.Clone();
                    }
                    clone.m_clauses[i] = query;
                }
            }
            if (clone != null)
            {
                return clone; // some clauses rewrote
            }
            else
            {
                return this; // no clauses rewrote
            }
        }

        public override object Clone()
        {
            int sz = m_clauses.Count;
            SpanQuery[] newClauses = new SpanQuery[sz];

            for (int i = 0; i < sz; i++)
            {
                newClauses[i] = (SpanQuery)m_clauses[i].Clone();
            }
            SpanNearQuery spanNearQuery = new SpanNearQuery(newClauses, m_slop, m_inOrder);
            spanNearQuery.Boost = Boost;
            return spanNearQuery;
        }

        /// <summary>
        /// Returns true iff <code>o</code> is equal to this. </summary>
        public override bool Equals(object o)
        {
            if (this == o)
            {
                return true;
            }
            if (!(o is SpanNearQuery spanNearQuery))
            {
                return false;
            }

            if (m_inOrder != spanNearQuery.m_inOrder)
            {
                return false;
            }
            if (m_slop != spanNearQuery.m_slop)
            {
                return false;
            }
            if (!JCG.ListEqualityComparer<SpanQuery>.Default.Equals(m_clauses, spanNearQuery.m_clauses))
            {
                return false;
            }

            return Boost == spanNearQuery.Boost;
        }

        public override int GetHashCode()
        {
            int result;
            result = JCG.ListEqualityComparer<SpanQuery>.Default.GetHashCode(m_clauses);
            // Mix bits before folding in things like boost, since it could cancel the
            // last element of clauses.  this particular mix also serves to
            // differentiate SpanNearQuery hashcodes from others.
            result ^= (result << 14) | ((int)((uint)result >> 19)); // reversible
            result += J2N.BitConversion.SingleToRawInt32Bits(Boost);
            result += m_slop;
            result ^= (m_inOrder ? unchecked((int)0x99AFD3BD) : 0);
            return result;
        }
    }
}