using Lucene.Net.Support;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Lucene.Net.Search
{
    /*
     * 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>
    /// Represents hits returned by 
    /// <see cref="IndexSearcher.Search(Query,Filter,int)"/> and 
    /// <see cref="IndexSearcher.Search(Query,int)"/>.
    /// </summary>
    public class TopDocs
    {
        /// <summary>
        /// The total number of hits for the query. </summary>
        public int TotalHits { get; set; }

        /// <summary>
        /// The top hits for the query. </summary>
        [WritableArray]
        [SuppressMessage("Microsoft.Performance", "CA1819", Justification = "Lucene's design requires some writable array properties")]
        public ScoreDoc[] ScoreDocs { get; set; }

        /// <summary>
        /// Stores the maximum score value encountered, needed for normalizing. </summary>
        private float maxScore;

        /// <summary>
        /// Returns the maximum score value encountered. Note that in case
        /// scores are not tracked, this returns <see cref="float.NaN"/>.
        /// </summary>
        public virtual float MaxScore
        {
            get => maxScore;
            set => this.maxScore = value;
        }

        /// <summary>
        /// Constructs a <see cref="TopDocs"/> with a default <c>maxScore=System.Single.NaN</c>. </summary>
        internal TopDocs(int totalHits, ScoreDoc[] scoreDocs)
            : this(totalHits, scoreDocs, float.NaN)
        {
        }

        public TopDocs(int totalHits, ScoreDoc[] scoreDocs, float maxScore)
        {
            this.TotalHits = totalHits;
            this.ScoreDocs = scoreDocs;
            this.maxScore = maxScore;
        }

        // Refers to one hit:
        private class ShardRef
        {
            // Which shard (index into shardHits[]):
            internal int ShardIndex { get; private set; }

            // Which hit within the shard:
            internal int HitIndex { get; set; }

            public ShardRef(int shardIndex)
            {
                this.ShardIndex = shardIndex;
            }

            public override string ToString()
            {
                return "ShardRef(shardIndex=" + ShardIndex + " hitIndex=" + HitIndex + ")";
            }
        }

        // Specialized MergeSortQueue that just merges by
        // relevance score, descending:
        private class ScoreMergeSortQueue : Util.PriorityQueue<ShardRef>
        {
            internal readonly ScoreDoc[][] shardHits;

            public ScoreMergeSortQueue(TopDocs[] shardHits)
                : base(shardHits.Length)
            {
                this.shardHits = new ScoreDoc[shardHits.Length][];
                for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++)
                {
                    this.shardHits[shardIDX] = shardHits[shardIDX].ScoreDocs;
                }
            }

            // Returns true if first is < second
            protected internal override bool LessThan(ShardRef first, ShardRef second)
            {
                Debug.Assert(first != second);
                float firstScore = shardHits[first.ShardIndex][first.HitIndex].Score;
                float secondScore = shardHits[second.ShardIndex][second.HitIndex].Score;

                if (firstScore < secondScore)
                {
                    return false;
                }
                else if (firstScore > secondScore)
                {
                    return true;
                }
                else
                {
                    // Tie break: earlier shard wins
                    if (first.ShardIndex < second.ShardIndex)
                    {
                        return true;
                    }
                    else if (first.ShardIndex > second.ShardIndex)
                    {
                        return false;
                    }
                    else
                    {
                        // Tie break in same shard: resolve however the
                        // shard had resolved it:
                        Debug.Assert(first.HitIndex != second.HitIndex);
                        return first.HitIndex < second.HitIndex;
                    }
                }
            }
        }

        private class MergeSortQueue : Util.PriorityQueue<ShardRef>
        {
            // These are really FieldDoc instances:
            internal readonly ScoreDoc[][] shardHits;

            internal readonly FieldComparer[] comparers;
            internal readonly int[] reverseMul;

            public MergeSortQueue(Sort sort, TopDocs[] shardHits)
                : base(shardHits.Length)
            {
                this.shardHits = new ScoreDoc[shardHits.Length][];
                for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++)
                {
                    ScoreDoc[] shard = shardHits[shardIDX].ScoreDocs;
                    //System.out.println("  init shardIdx=" + shardIDX + " hits=" + shard);
                    if (shard != null)
                    {
                        this.shardHits[shardIDX] = shard;
                        // Fail gracefully if API is misused:
                        for (int hitIDX = 0; hitIDX < shard.Length; hitIDX++)
                        {
                            ScoreDoc sd = shard[hitIDX];
                            if (!(sd is FieldDoc))
                            {
                                throw new ArgumentException("shard " + shardIDX + " was not sorted by the provided Sort (expected FieldDoc but got ScoreDoc)");
                            }
                            FieldDoc fd = (FieldDoc)sd;
                            if (fd.Fields == null)
                            {
                                throw new ArgumentException("shard " + shardIDX + " did not set sort field values (FieldDoc.fields is null); you must pass fillFields=true to IndexSearcher.search on each shard");
                            }
                        }
                    }
                }

                SortField[] sortFields = sort.GetSort();
                comparers = new FieldComparer[sortFields.Length];
                reverseMul = new int[sortFields.Length];
                for (int compIDX = 0; compIDX < sortFields.Length; compIDX++)
                {
                    SortField sortField = sortFields[compIDX];
                    comparers[compIDX] = sortField.GetComparer(1, compIDX);
                    reverseMul[compIDX] = sortField.IsReverse ? -1 : 1;
                }
            }

            // Returns true if first is < second
            protected internal override bool LessThan(ShardRef first, ShardRef second)
            {
                Debug.Assert(first != second);
                FieldDoc firstFD = (FieldDoc)shardHits[first.ShardIndex][first.HitIndex];
                FieldDoc secondFD = (FieldDoc)shardHits[second.ShardIndex][second.HitIndex];
                //System.out.println("  lessThan:\n     first=" + first + " doc=" + firstFD.doc + " score=" + firstFD.score + "\n    second=" + second + " doc=" + secondFD.doc + " score=" + secondFD.score);

                for (int compIDX = 0; compIDX < comparers.Length; compIDX++)
                {
                    FieldComparer comp = comparers[compIDX];
                    //System.out.println("    cmp idx=" + compIDX + " cmp1=" + firstFD.fields[compIDX] + " cmp2=" + secondFD.fields[compIDX] + " reverse=" + reverseMul[compIDX]);

                    int cmp = reverseMul[compIDX] * comp.CompareValues(firstFD.Fields[compIDX], secondFD.Fields[compIDX]);

                    if (cmp != 0)
                    {
                        //System.out.println("    return " + (cmp < 0));
                        return cmp < 0;
                    }
                }

                // Tie break: earlier shard wins
                if (first.ShardIndex < second.ShardIndex)
                {
                    //System.out.println("    return tb true");
                    return true;
                }
                else if (first.ShardIndex > second.ShardIndex)
                {
                    //System.out.println("    return tb false");
                    return false;
                }
                else
                {
                    // Tie break in same shard: resolve however the
                    // shard had resolved it:
                    //System.out.println("    return tb " + (first.hitIndex < second.hitIndex));
                    Debug.Assert(first.HitIndex != second.HitIndex);
                    return first.HitIndex < second.HitIndex;
                }
            }
        }

        /// <summary>
        /// Returns a new <see cref="TopDocs"/>, containing <paramref name="topN"/> results across
        /// the provided <see cref="TopDocs"/>, sorting by the specified 
        /// <see cref="Sort"/>.  Each of the <see cref="TopDocs"/> must have been sorted by
        /// the same <see cref="Sort"/>, and sort field values must have been
        /// filled (ie, <c>fillFields=true</c> must be
        /// passed to
        /// <see cref="TopFieldCollector.Create(Sort, int, bool, bool, bool, bool)"/>.
        ///
        /// <para/>Pass <paramref name="sort"/>=null to merge sort by score descending.
        /// <para/>
        /// @lucene.experimental
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static TopDocs Merge(Sort sort, int topN, TopDocs[] shardHits)
        {
            return Merge(sort, 0, topN, shardHits);
        }

        /// <summary>
        /// Same as <see cref="Merge(Sort, int, TopDocs[])"/> but also slices the result at the same time based
        /// on the provided start and size. The return <c>TopDocs</c> will always have a scoreDocs with length of 
        /// at most <see cref="Util.PriorityQueue{T}.Count"/>.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static TopDocs Merge(Sort sort, int start, int size, TopDocs[] shardHits)
        {
            Util.PriorityQueue<ShardRef> queue;
            if (sort == null)
            {
                queue = new ScoreMergeSortQueue(shardHits);
            }
            else
            {
                queue = new MergeSortQueue(sort, shardHits);
            }

            int totalHitCount = 0;
            int availHitCount = 0;
            float maxScore = float.MinValue;
            for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++)
            {
                TopDocs shard = shardHits[shardIDX];
                // totalHits can be non-zero even if no hits were
                // collected, when searchAfter was used:
                totalHitCount += shard.TotalHits;
                if (shard.ScoreDocs != null && shard.ScoreDocs.Length > 0)
                {
                    availHitCount += shard.ScoreDocs.Length;
                    queue.Add(new ShardRef(shardIDX));
                    maxScore = Math.Max(maxScore, shard.MaxScore);
                    //System.out.println("  maxScore now " + maxScore + " vs " + shard.getMaxScore());
                }
            }

            if (availHitCount == 0)
            {
                maxScore = float.NaN;
            }

            ScoreDoc[] hits;
            if (availHitCount <= start)
            {
                hits = Arrays.Empty<ScoreDoc>();
            }
            else
            {
                hits = new ScoreDoc[Math.Min(size, availHitCount - start)];
                int requestedResultWindow = start + size;
                int numIterOnHits = Math.Min(availHitCount, requestedResultWindow);
                int hitUpto = 0;
                while (hitUpto < numIterOnHits)
                {
                    Debug.Assert(queue.Count > 0);
                    ShardRef @ref = queue.Pop();
                    ScoreDoc hit = shardHits[@ref.ShardIndex].ScoreDocs[@ref.HitIndex++];
                    hit.ShardIndex = @ref.ShardIndex;
                    if (hitUpto >= start)
                    {
                        hits[hitUpto - start] = hit;
                    }

                    //System.out.println("  hitUpto=" + hitUpto);
                    //System.out.println("    doc=" + hits[hitUpto].doc + " score=" + hits[hitUpto].score);

                    hitUpto++;

                    if (@ref.HitIndex < shardHits[@ref.ShardIndex].ScoreDocs.Length)
                    {
                        // Not done with this these TopDocs yet:
                        queue.Add(@ref);
                    }
                }
            }

            if (sort == null)
            {
                return new TopDocs(totalHitCount, hits, maxScore);
            }
            else
            {
                return new TopFieldDocs(totalHitCount, hits, sort.GetSort(), maxScore);
            }
        }
    }
}