using System.Collections.Generic;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Util;

namespace Lucene.Net.Join
{
    /*
     * 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 query that has an array of terms from a specific field. This query will match documents have one or more terms in
    /// the specified field that match with the terms specified in the array.
    /// 
    /// @lucene.experimental
    /// </summary>
    internal class TermsQuery : MultiTermQuery
    {
        private readonly BytesRefHash _terms;
        private readonly int[] _ords;
        private readonly Query _fromQuery; // Used for equals() only

        /// <summary>
        /// 
        /// </summary>
        /// <param name="field">The field that should contain terms that are specified in the previous parameter.</param>
        /// <param name="fromQuery"></param>
        /// <param name="terms">The terms that matching documents should have. The terms must be sorted by natural order.</param>
        internal TermsQuery(string field, Query fromQuery, BytesRefHash terms) 
            : base(field)
        {
            _fromQuery = fromQuery;
            _terms = terms;
            _ords = terms.Sort(BytesRef.UTF8SortedAsUnicodeComparer);
        }

        protected override TermsEnum GetTermsEnum(Terms terms, AttributeSource atts)
        {
            if (_terms.Count == 0)
            {
                return TermsEnum.EMPTY;
            }

            return new SeekingTermSetTermsEnum(terms.GetIterator(null), _terms, _ords);

        }

        public override string ToString(string field)
        {
            return string.Format("TermsQuery{{field={0}}}", field);
        }

        public override bool Equals(object obj)
        {
            if (this == obj)
            {
                return true;
            }
            if (!base.Equals(obj))
            {
                return false;
            }
            if (GetType() != obj.GetType())
            {
                return false;
            }

            TermsQuery other = (TermsQuery)obj;
            if (!_fromQuery.Equals(other._fromQuery))
            {
                return false;
            }
            return true;
        }

        public override int GetHashCode()
        {
            int prime = 31;
            int result = base.GetHashCode();
            result += prime * _fromQuery.GetHashCode();
            return result;
        }

        private class SeekingTermSetTermsEnum : FilteredTermsEnum
        {
            private readonly BytesRefHash terms;
            private readonly int[] ords;
            private readonly int _lastElement;

            private readonly BytesRef _lastTerm;
            private readonly BytesRef _spare = new BytesRef();
            private readonly IComparer<BytesRef> _comparer;

            private BytesRef _seekTerm;
            private int _upto;

            internal SeekingTermSetTermsEnum(TermsEnum tenum, BytesRefHash terms, int[] ords) 
                : base(tenum)
            {
                this.terms = terms;
                this.ords = ords;
                _comparer = BytesRef.UTF8SortedAsUnicodeComparer;
                _lastElement = terms.Count - 1;
                _lastTerm = terms.Get(ords[_lastElement], new BytesRef());
                _seekTerm = terms.Get(ords[_upto], _spare);
            }

            protected override BytesRef NextSeekTerm(BytesRef currentTerm)
            {
                BytesRef temp = _seekTerm;
                _seekTerm = null;
                return temp;
            }
            
            protected override AcceptStatus Accept(BytesRef term)
            {
                if (_comparer.Compare(term, _lastTerm) > 0)
                {
                    return AcceptStatus.END;
                }

                BytesRef currentTerm = terms.Get(ords[_upto], _spare);
                if (_comparer.Compare(term, currentTerm) == 0)
                {
                    if (_upto == _lastElement)
                    {
                        return AcceptStatus.YES;
                    }

                    _seekTerm = terms.Get(ords[++_upto], _spare);
                    return AcceptStatus.YES_AND_SEEK;
                }

                if (_upto == _lastElement)
                {
                    return AcceptStatus.NO;
                } // Our current term doesn't match the the given term.

                int cmp;
                do // We maybe are behind the given term by more than one step. Keep incrementing till we're the same or higher.
                {
                    if (_upto == _lastElement)
                    {
                        return AcceptStatus.NO;
                    }
                    // typically the terms dict is a superset of query's terms so it's unusual that we have to skip many of
                    // our terms so we don't do a binary search here
                    _seekTerm = terms.Get(ords[++_upto], _spare);
                } while ((cmp = _comparer.Compare(_seekTerm, term)) < 0);
                if (cmp == 0)
                {
                    if (_upto == _lastElement)
                    {
                        return AcceptStatus.YES;
                    }
                    _seekTerm = terms.Get(ords[++_upto], _spare);
                    return AcceptStatus.YES_AND_SEEK;
                }

                return AcceptStatus.NO_AND_SEEK;
            }
        }
    }
}