/* 
 * 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 System;
using System.Collections.Generic;
using System.Linq;
using Lucene.Net.Support;
using Lucene.Net.Util;
using Document = Lucene.Net.Documents.Document;
using FieldSelector = Lucene.Net.Documents.FieldSelector;
using CorruptIndexException = Lucene.Net.Index.CorruptIndexException;
using IndexReader = Lucene.Net.Index.IndexReader;
using Term = Lucene.Net.Index.Term;
using ReaderUtil = Lucene.Net.Util.ReaderUtil;

namespace Lucene.Net.Search
{
	
	/// <summary>Implements search over a set of <c>Searchables</c>.
	/// 
	/// <p/>Applications usually need only call the inherited <see cref="Searcher.Search(Query, int)" />
	/// or <see cref="Searcher.Search(Query,Filter, int)" /> methods.
	/// </summary>
	public class MultiSearcher:Searcher
	{
		private class AnonymousClassCollector:Collector
		{
			public AnonymousClassCollector(Lucene.Net.Search.Collector collector, int start, MultiSearcher enclosingInstance)
			{
				InitBlock(collector, start, enclosingInstance);
			}
			private void  InitBlock(Lucene.Net.Search.Collector collector, int start, MultiSearcher enclosingInstance)
			{
				this.collector = collector;
				this.start = start;
				this.enclosingInstance = enclosingInstance;
			}
			private Lucene.Net.Search.Collector collector;
			private int start;
			private MultiSearcher enclosingInstance;
			public MultiSearcher Enclosing_Instance
			{
				get
				{
					return enclosingInstance;
				}
				
			}
			public override void  SetScorer(Scorer scorer)
			{
				collector.SetScorer(scorer);
			}
			public override void  Collect(int doc)
			{
				collector.Collect(doc);
			}
			public override void  SetNextReader(IndexReader reader, int docBase)
			{
				collector.SetNextReader(reader, start + docBase);
			}

		    public override bool AcceptsDocsOutOfOrder
		    {
		        get { return collector.AcceptsDocsOutOfOrder; }
		    }
		}
		
		/// <summary> Document Frequency cache acting as a Dummy-Searcher. This class is no
		/// full-fledged Searcher, but only supports the methods necessary to
		/// initialize Weights.
		/// </summary>
		private class CachedDfSource:Searcher
		{
			private readonly Dictionary<Term,int> dfMap; // Map from Terms to corresponding doc freqs
			private readonly int maxDoc; // document count
			
			public CachedDfSource(Dictionary<Term,int> dfMap, int maxDoc, Similarity similarity)
			{
				this.dfMap = dfMap;
				this.maxDoc = maxDoc;
				Similarity = similarity;
			}
			
			public override int DocFreq(Term term)
			{
				int df;
				try
				{
					df = dfMap[term];
				}
				catch (KeyNotFoundException) // C# equiv. of java code.
				{
					throw new System.ArgumentException("df for term " + term.Text + " not available");
				}
				return df;
			}
			
			public override int[] DocFreqs(Term[] terms)
			{
				int[] result = new int[terms.Length];
				for (int i = 0; i < terms.Length; i++)
				{
					result[i] = DocFreq(terms[i]);
				}
				return result;
			}
			
			public override int MaxDoc
			{
                get { return maxDoc; }
			}
			
			public override Query Rewrite(Query query)
			{
				// this is a bit of a hack. We know that a query which
				// creates a Weight based on this Dummy-Searcher is
				// always already rewritten (see preparedWeight()).
				// Therefore we just return the unmodified query here
				return query;
			}

            // TODO: This probably shouldn't throw an exception?
            protected override void Dispose(bool disposing)
            {
                throw new System.NotSupportedException();
            }
			
			public override Document Doc(int i)
			{
				throw new System.NotSupportedException();
			}
			
			public override Document Doc(int i, FieldSelector fieldSelector)
			{
				throw new System.NotSupportedException();
			}
			
			public override Explanation Explain(Weight weight, int doc)
			{
				throw new System.NotSupportedException();
			}
			
			public override void  Search(Weight weight, Filter filter, Collector results)
			{
				throw new System.NotSupportedException();
			}
			
			public override TopDocs Search(Weight weight, Filter filter, int n)
			{
				throw new System.NotSupportedException();
			}
			
			public override TopFieldDocs Search(Weight weight, Filter filter, int n, Sort sort)
			{
				throw new System.NotSupportedException();
			}
		}
		
		private Searchable[] searchables;
		private int[] starts;
		private int maxDoc = 0;

	    private bool isDisposed;
		
		/// <summary>Creates a searcher which searches <i>searchers</i>. </summary>
		public MultiSearcher(params Searchable[] searchables)
		{
			this.searchables = searchables;
			
			starts = new int[searchables.Length + 1]; // build starts array
			for (int i = 0; i < searchables.Length; i++)
			{
				starts[i] = maxDoc;
				maxDoc += searchables[i].MaxDoc; // compute maxDocs
			}
			starts[searchables.Length] = maxDoc;
		}
		
		/// <summary>Return the array of <see cref="Searchable" />s this searches. </summary>
		public virtual Searchable[] GetSearchables()
		{
			return searchables;
		}
		
		protected internal virtual int[] GetStarts()
		{
			return starts;
		}

        protected override void Dispose(bool disposing)
        {
            if (isDisposed) return;

            if (disposing)
            {
                for (int i = 0; i < searchables.Length; i++)
                    searchables[i].Close();
            }

            isDisposed = true;
        }

		public override int DocFreq(Term term)
		{
			int docFreq = 0;
			for (int i = 0; i < searchables.Length; i++)
				docFreq += searchables[i].DocFreq(term);
			return docFreq;
		}
		
		// inherit javadoc
		public override Document Doc(int n)
		{
			int i = SubSearcher(n); // find searcher index
			return searchables[i].Doc(n - starts[i]); // dispatch to searcher
		}
		
		// inherit javadoc
		public override Document Doc(int n, FieldSelector fieldSelector)
		{
			int i = SubSearcher(n); // find searcher index
			return searchables[i].Doc(n - starts[i], fieldSelector); // dispatch to searcher
		}
		
		/// <summary>Returns index of the searcher for document <c>n</c> in the array
		/// used to construct this searcher. 
		/// </summary>
		public virtual int SubSearcher(int n)
		{
			// find searcher for doc n:
			return ReaderUtil.SubIndex(n, starts);
		}
		
		/// <summary>Returns the document number of document <c>n</c> within its
		/// sub-index. 
		/// </summary>
		public virtual int SubDoc(int n)
		{
			return n - starts[SubSearcher(n)];
		}

	    public override int MaxDoc
	    {
	        get { return maxDoc; }
	    }

	    public override TopDocs Search(Weight weight, Filter filter, int nDocs)
		{
			HitQueue hq = new HitQueue(nDocs, false);
			int totalHits = 0;

            var lockObj = new object();
			for (int i = 0; i < searchables.Length; i++)
			{
                // search each searcher
                // use NullLock, we don't care about synchronization for these
                TopDocs docs = MultiSearcherCallableNoSort(ThreadLock.NullLock, lockObj, searchables[i], weight, filter, nDocs, hq, i, starts);
				totalHits += docs.TotalHits; // update totalHits
			}
			
			ScoreDoc[] scoreDocs2 = new ScoreDoc[hq.Size()];
			for (int i = hq.Size() - 1; i >= 0; i--)
			// put docs in array
				scoreDocs2[i] = hq.Pop();
			
			float maxScore = (totalHits == 0)?System.Single.NegativeInfinity:scoreDocs2[0].Score;
			
			return new TopDocs(totalHits, scoreDocs2, maxScore);
		}
		
		public override TopFieldDocs Search(Weight weight, Filter filter, int n, Sort sort)
		{
			var hq = new FieldDocSortedHitQueue(n);
			int totalHits = 0;
			
			float maxScore = System.Single.NegativeInfinity;

		    var lockObj = new object();
			for (int i = 0; i < searchables.Length; i++)
			{
				// search each searcher
                // use NullLock, we don't care about synchronization for these
                TopFieldDocs docs = MultiSearcherCallableWithSort(ThreadLock.NullLock, lockObj, searchables[i], weight, filter, n, hq, sort,
			                                          i, starts);
			    totalHits += docs.TotalHits;
				maxScore = System.Math.Max(maxScore, docs.MaxScore);
			}
			
			ScoreDoc[] scoreDocs2 = new ScoreDoc[hq.Size()];
			for (int i = hq.Size() - 1; i >= 0; i--)
			// put docs in array
				scoreDocs2[i] = hq.Pop();
			
			return new TopFieldDocs(totalHits, scoreDocs2, hq.GetFields(), maxScore);
		}
		
		///<inheritdoc />
		public override void  Search(Weight weight, Filter filter, Collector collector)
		{
			for (int i = 0; i < searchables.Length; i++)
			{
				int start = starts[i];
				
				Collector hc = new AnonymousClassCollector(collector, start, this);
				searchables[i].Search(weight, filter, hc);
			}
		}
		
		public override Query Rewrite(Query original)
		{
			Query[] queries = new Query[searchables.Length];
			for (int i = 0; i < searchables.Length; i++)
			{
				queries[i] = searchables[i].Rewrite(original);
			}
			return queries[0].Combine(queries);
		}
		
		public override Explanation Explain(Weight weight, int doc)
		{
			int i = SubSearcher(doc); // find searcher index
			return searchables[i].Explain(weight, doc - starts[i]); // dispatch to searcher
		}
		
		/// <summary> Create weight in multiple index scenario.
		/// 
		/// Distributed query processing is done in the following steps:
		/// 1. rewrite query
		/// 2. extract necessary terms
		/// 3. collect dfs for these terms from the Searchables
		/// 4. create query weight using aggregate dfs.
		/// 5. distribute that weight to Searchables
		/// 6. merge results
		/// 
		/// Steps 1-4 are done here, 5+6 in the search() methods
		/// 
		/// </summary>
		/// <returns> rewritten queries
		/// </returns>
		public /*protected internal*/ override Weight CreateWeight(Query original)
		{
			// step 1
			Query rewrittenQuery = Rewrite(original);
			
			// step 2
		    ISet<Term> terms = Lucene.Net.Support.Compatibility.SetFactory.CreateHashSet<Term>();
			rewrittenQuery.ExtractTerms(terms);
			
			// step3
		    Term[] allTermsArray = terms.ToArray();
            int[] aggregatedDfs = new int[terms.Count];
			for (int i = 0; i < searchables.Length; i++)
			{
				int[] dfs = searchables[i].DocFreqs(allTermsArray);
				for (int j = 0; j < aggregatedDfs.Length; j++)
				{
					aggregatedDfs[j] += dfs[j];
				}
			}
			
			var dfMap = new Dictionary<Term, int>();
			for (int i = 0; i < allTermsArray.Length; i++)
			{
				dfMap[allTermsArray[i]] = aggregatedDfs[i];
			}
			
			// step4
			int numDocs = MaxDoc;
			CachedDfSource cacheSim = new CachedDfSource(dfMap, numDocs, Similarity);
			
			return rewrittenQuery.Weight(cacheSim);
		}

	    internal Func<ThreadLock, object, Searchable, Weight, Filter, int, HitQueue, int, int[], TopDocs> MultiSearcherCallableNoSort =
	        (threadLock, lockObj, searchable, weight, filter, nDocs, hq, i, starts) =>
	            {
	                TopDocs docs = searchable.Search(weight, filter, nDocs);
	                ScoreDoc[] scoreDocs = docs.ScoreDocs;
                    for(int j = 0; j < scoreDocs.Length; j++) // merge scoreDocs into hq
                    {
                        ScoreDoc scoreDoc = scoreDocs[j];
                        scoreDoc.Doc += starts[i]; //convert doc
                        //it would be so nice if we had a thread-safe insert
                        try
                        {
                            threadLock.Enter(lockObj);
                            if (scoreDoc == hq.InsertWithOverflow(scoreDoc))
                                break;
                        }
                        finally
                        {
                            threadLock.Exit(lockObj);
                        }
                    }
	                return docs;
	            };

        internal Func<ThreadLock, object, Searchable, Weight, Filter, int, FieldDocSortedHitQueue, Sort, int, int[], TopFieldDocs>
            MultiSearcherCallableWithSort = (threadLock, lockObj, searchable, weight, filter, nDocs, hq, sort, i, starts) =>
	                                            {
	                                                TopFieldDocs docs = searchable.Search(weight, filter, nDocs, sort);
                                                    // if one of the Sort fields is FIELD_DOC, need to fix its values, so that
                                                    // it will break ties by doc Id properly.  Otherwise, it will compare to
                                                    // 'relative' doc Ids, that belong to two different searchables.
                                                    for (int j = 0; j < docs.fields.Length; j++)
                                                    {
                                                        if (docs.fields[j].Type == SortField.DOC)
                                                        {
                                                            // iterate over the score docs and change their fields value
                                                            for (int j2 = 0; j2 < docs.ScoreDocs.Length; j2++)
                                                            {
                                                                FieldDoc fd = (FieldDoc) docs.ScoreDocs[j2];
                                                                fd.fields[j] = (int)fd.fields[j] + starts[i];
                                                            }
                                                            break;
                                                        }
                                                    }
	                                                try
                                                    {
                                                        threadLock.Enter(lockObj);
                                                        hq.SetFields(docs.fields);
	                                                }
	                                                finally
                                                    {
                                                        threadLock.Exit(lockObj);
	                                                }

	                                                ScoreDoc[] scoreDocs = docs.ScoreDocs;
                                                    for (int j = 0; j < scoreDocs.Length; j++) // merge scoreDocs into hq
                                                    {
                                                        FieldDoc fieldDoc = (FieldDoc) scoreDocs[j];
                                                        fieldDoc.Doc += starts[i]; //convert doc
                                                        //it would be so nice if we had a thread-safe insert
                                                        lock (lockObj)
                                                        {
                                                            if (fieldDoc == hq.InsertWithOverflow(fieldDoc))
                                                                break;

                                                        }
                                                    }
	                                                return docs;
	                                            };
	}
}