/* 
 * 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 Document = Lucene.Net.Documents.Document;
using FieldSelector = Lucene.Net.Documents.FieldSelector;
using FieldSelectorResult = Lucene.Net.Documents.FieldSelectorResult;

namespace Lucene.Net.Index
{
	/// <summary>An IndexReader which reads multiple, parallel indexes.  Each index added
	/// must have the same number of documents, but typically each contains
	/// different fields.  Each document contains the union of the fields of all
	/// documents with the same document number.  When searching, matches for a
	/// query term are from the first index added that has the field.
	/// 
	/// <p/>This is useful, e.g., with collections that have large fields which
	/// change rarely and small fields that change more frequently.  The smaller
	/// fields may be re-indexed in a new index and both indexes may be searched
	/// together.
	/// 
	/// <p/><strong>Warning:</strong> It is up to you to make sure all indexes
	/// are created and modified the same way. For example, if you add
	/// documents to one index, you need to add the same documents in the
	/// same order to the other indexes. <em>Failure to do so will result in
	/// undefined behavior</em>.
	/// </summary>
	public class ParallelReader:IndexReader, System.ICloneable
	{
        private List<IndexReader> readers = new List<IndexReader>();
        private List<bool> decrefOnClose = new List<bool>(); // remember which subreaders to decRef on close
		internal bool incRefReaders = false;
		private SortedDictionary<string, IndexReader> fieldToReader = new SortedDictionary<string, IndexReader>();
		private IDictionary<IndexReader, ICollection<string>> readerToFields = new HashMap<IndexReader, ICollection<string>>();
        private List<IndexReader> storedFieldReaders = new List<IndexReader>();
		
		private int maxDoc;
		private int numDocs;
		private bool hasDeletions;
		
		/// <summary>Construct a ParallelReader. 
		/// <p/>Note that all subreaders are closed if this ParallelReader is closed.<p/>
		/// </summary>
		public ParallelReader():this(true)
		{
		}
		
		/// <summary>Construct a ParallelReader. </summary>
		/// <param name="closeSubReaders">indicates whether the subreaders should be closed
		/// when this ParallelReader is closed
		/// </param>
		public ParallelReader(bool closeSubReaders):base()
		{
			this.incRefReaders = !closeSubReaders;
		}
		
		/// <summary>Add an IndexReader.</summary>
		/// <throws>  IOException if there is a low-level IO error </throws>
		public virtual void  Add(IndexReader reader)
		{
			EnsureOpen();
			Add(reader, false);
		}
		
		/// <summary>Add an IndexReader whose stored fields will not be returned.  This can
		/// accellerate search when stored fields are only needed from a subset of
		/// the IndexReaders.
		/// 
		/// </summary>
		/// <throws>  IllegalArgumentException if not all indexes contain the same number </throws>
		/// <summary>     of documents
		/// </summary>
		/// <throws>  IllegalArgumentException if not all indexes have the same value </throws>
		/// <summary>     of <see cref="IndexReader.MaxDoc" />
		/// </summary>
		/// <throws>  IOException if there is a low-level IO error </throws>
		public virtual void  Add(IndexReader reader, bool ignoreStoredFields)
		{
			
			EnsureOpen();
			if (readers.Count == 0)
			{
				this.maxDoc = reader.MaxDoc;
				this.numDocs = reader.NumDocs();
				this.hasDeletions = reader.HasDeletions;
			}
			
			if (reader.MaxDoc != maxDoc)
			// check compatibility
				throw new System.ArgumentException("All readers must have same maxDoc: " + maxDoc + "!=" + reader.MaxDoc);
			if (reader.NumDocs() != numDocs)
				throw new System.ArgumentException("All readers must have same numDocs: " + numDocs + "!=" + reader.NumDocs());
			
			ICollection<string> fields = reader.GetFieldNames(IndexReader.FieldOption.ALL);
			readerToFields[reader] = fields;
			foreach(var field in fields)
			{
				// update fieldToReader map
                // Do a containskey firt to mimic java behavior
				if (!fieldToReader.ContainsKey(field) || fieldToReader[field] == null)
					fieldToReader[field] = reader;
			}
			
			if (!ignoreStoredFields)
				storedFieldReaders.Add(reader); // add to storedFieldReaders
			readers.Add(reader);
			
			if (incRefReaders)
			{
				reader.IncRef();
			}
			decrefOnClose.Add(incRefReaders);
		}
		
		public override System.Object Clone()
		{
			try
			{
				return DoReopen(true);
			}
			catch (System.Exception ex)
			{
				throw new System.SystemException(ex.Message, ex);
			}
		}
		
		/// <summary> Tries to reopen the subreaders.
		/// <br/>
		/// If one or more subreaders could be re-opened (i. e. subReader.reopen() 
		/// returned a new instance != subReader), then a new ParallelReader instance 
		/// is returned, otherwise this instance is returned.
		/// <p/>
		/// A re-opened instance might share one or more subreaders with the old 
		/// instance. Index modification operations result in undefined behavior
		/// when performed before the old instance is closed.
		/// (see <see cref="IndexReader.Reopen()" />).
		/// <p/>
		/// If subreaders are shared, then the reference count of those
		/// readers is increased to ensure that the subreaders remain open
		/// until the last referring reader is closed.
		/// 
		/// </summary>
		/// <throws>  CorruptIndexException if the index is corrupt </throws>
		/// <throws>  IOException if there is a low-level IO error  </throws>
		public override IndexReader Reopen()
		{
			lock (this)
			{
				return DoReopen(false);
			}
		}
		
		protected internal virtual IndexReader DoReopen(bool doClone)
		{
			EnsureOpen();
			
			bool reopened = false;
            IList<IndexReader> newReaders = new List<IndexReader>();
			
			bool success = false;
			
			try
			{
				foreach(var oldReader in readers)
				{
					IndexReader newReader = null;
					if (doClone)
					{
						newReader = (IndexReader) oldReader.Clone();
					}
					else
					{
						newReader = oldReader.Reopen();
					}
					newReaders.Add(newReader);
					// if at least one of the subreaders was updated we remember that
					// and return a new ParallelReader
					if (newReader != oldReader)
					{
						reopened = true;
					}
				}
				success = true;
			}
			finally
			{
				if (!success && reopened)
				{
					for (int i = 0; i < newReaders.Count; i++)
					{
						IndexReader r = newReaders[i];
						if (r != readers[i])
						{
							try
							{
								r.Close();
							}
							catch (System.IO.IOException)
							{
								// keep going - we want to clean up as much as possible
							}
						}
					}
				}
			}
			
			if (reopened)
			{
                List<bool> newDecrefOnClose = new List<bool>();
				ParallelReader pr = new ParallelReader();
				for (int i = 0; i < readers.Count; i++)
				{
					IndexReader oldReader = readers[i];
					IndexReader newReader = newReaders[i];
					if (newReader == oldReader)
					{
						newDecrefOnClose.Add(true);
						newReader.IncRef();
					}
					else
					{
						// this is a new subreader instance, so on close() we don't
						// decRef but close it 
						newDecrefOnClose.Add(false);
					}
					pr.Add(newReader, !storedFieldReaders.Contains(oldReader));
				}
				pr.decrefOnClose = newDecrefOnClose;
				pr.incRefReaders = incRefReaders;
				return pr;
			}
			else
			{
				// No subreader was refreshed
				return this;
			}
		}


	    public override int NumDocs()
	    {
	        // Don't call ensureOpen() here (it could affect performance)
	        return numDocs;
	    }

	    public override int MaxDoc
	    {
	        get
	        {
	            // Don't call ensureOpen() here (it could affect performance)
	            return maxDoc;
	        }
	    }

	    public override bool HasDeletions
	    {
	        get
	        {
	            // Don't call ensureOpen() here (it could affect performance)
	            return hasDeletions;
	        }
	    }

	    // check first reader
		public override bool IsDeleted(int n)
		{
			// Don't call ensureOpen() here (it could affect performance)
			if (readers.Count > 0)
				return readers[0].IsDeleted(n);
			return false;
		}
		
		// delete in all readers
		protected internal override void  DoDelete(int n)
		{
			foreach(var reader in readers)
			{
				reader.DeleteDocument(n);
			}
			hasDeletions = true;
		}
		
		// undeleteAll in all readers
		protected internal override void  DoUndeleteAll()
		{
			foreach(var reader in readers)
			{
				reader.UndeleteAll();
			}
			hasDeletions = false;
		}
		
		// append fields from storedFieldReaders
		public override Document Document(int n, FieldSelector fieldSelector)
		{
			EnsureOpen();
			Document result = new Document();
			foreach(IndexReader reader in storedFieldReaders)
			{
				bool include = (fieldSelector == null);
				if (!include)
				{
				    var fields = readerToFields[reader];
					foreach(var field in fields)
					{
                        if (fieldSelector.Accept(field) != FieldSelectorResult.NO_LOAD)
						{
							include = true;
							break;
						}
					}
				}
				if (include)
				{
				    var fields = reader.Document(n, fieldSelector).GetFields();
					foreach(var field in fields)
					{
                        result.Add(field);
					}
				}
			}
			return result;
		}
		
		// get all vectors
		public override ITermFreqVector[] GetTermFreqVectors(int n)
		{
			EnsureOpen();
			IList<ITermFreqVector> results = new List<ITermFreqVector>();
            foreach(var e in fieldToReader)
			{
				System.String field = e.Key;
				IndexReader reader = e.Value;

				ITermFreqVector vector = reader.GetTermFreqVector(n, field);
				if (vector != null)
					results.Add(vector);
			}
			return results.ToArray();
		}
		
		public override ITermFreqVector GetTermFreqVector(int n, System.String field)
		{
			EnsureOpen();
			IndexReader reader = (fieldToReader[field]);
			return reader == null?null:reader.GetTermFreqVector(n, field);
		}
		
		
		public override void  GetTermFreqVector(int docNumber, System.String field, TermVectorMapper mapper)
		{
			EnsureOpen();
			IndexReader reader = (fieldToReader[field]);
			if (reader != null)
			{
				reader.GetTermFreqVector(docNumber, field, mapper);
			}
		}
		
		public override void  GetTermFreqVector(int docNumber, TermVectorMapper mapper)
		{
			EnsureOpen();

            foreach(var e in fieldToReader)
			{
				System.String field = e.Key;
				IndexReader reader = e.Value;
				reader.GetTermFreqVector(docNumber, field, mapper);
			}
		}
		
		public override bool HasNorms(System.String field)
		{
			EnsureOpen();
			IndexReader reader = fieldToReader[field];
		    return reader != null && reader.HasNorms(field);
		}
		
		public override byte[] Norms(System.String field)
		{
			EnsureOpen();
			IndexReader reader = fieldToReader[field];
			return reader == null?null:reader.Norms(field);
		}
		
		public override void  Norms(System.String field, byte[] result, int offset)
		{
			EnsureOpen();
			IndexReader reader = fieldToReader[field];
			if (reader != null)
				reader.Norms(field, result, offset);
		}
		
		protected internal override void  DoSetNorm(int n, System.String field, byte value_Renamed)
		{
			IndexReader reader = fieldToReader[field];
			if (reader != null)
				reader.DoSetNorm(n, field, value_Renamed);
		}
		
		public override TermEnum Terms()
		{
			EnsureOpen();
			return new ParallelTermEnum(this);
		}
		
		public override TermEnum Terms(Term term)
		{
			EnsureOpen();
			return new ParallelTermEnum(this, term);
		}
		
		public override int DocFreq(Term term)
		{
			EnsureOpen();
			IndexReader reader = fieldToReader[term.Field];
			return reader == null?0:reader.DocFreq(term);
		}
		
		public override TermDocs TermDocs(Term term)
		{
			EnsureOpen();
			return new ParallelTermDocs(this, term);
		}
		
		public override TermDocs TermDocs()
		{
			EnsureOpen();
			return new ParallelTermDocs(this);
		}
		
		public override TermPositions TermPositions(Term term)
		{
			EnsureOpen();
			return new ParallelTermPositions(this, term);
		}
		
		public override TermPositions TermPositions()
		{
			EnsureOpen();
			return new ParallelTermPositions(this);
		}

	    /// <summary> Checks recursively if all subreaders are up to date. </summary>
	    public override bool IsCurrent()
	    {
	        foreach (var reader in readers)
	        {
	            if (!reader.IsCurrent())
	            {
	                return false;
	            }
	        }

	        // all subreaders are up to date
	        return true;
	    }

	    /// <summary> Checks recursively if all subindexes are optimized </summary>
	    public override bool IsOptimized()
	    {
	        foreach (var reader in readers)
	        {
	            if (!reader.IsOptimized())
	            {
	                return false;
	            }
	        }

	        // all subindexes are optimized
	        return true;
	    }


	    /// <summary>Not implemented.</summary>
	    /// <throws>  UnsupportedOperationException </throws>
	    public override long Version
	    {
	        get { throw new System.NotSupportedException("ParallelReader does not support this method."); }
	    }

	    // for testing
		public /*internal*/ virtual IndexReader[] GetSubReaders()
		{
			return readers.ToArray();
		}

        protected internal override void DoCommit(IDictionary<string, string> commitUserData)
		{
			foreach(var reader in readers)
				reader.Commit(commitUserData);
		}
		
		protected internal override void  DoClose()
		{
			lock (this)
			{
				for (int i = 0; i < readers.Count; i++)
				{
					if (decrefOnClose[i])
					{
						readers[i].DecRef();
					}
					else
					{
						readers[i].Close();
					}
				}
			}

            Lucene.Net.Search.FieldCache_Fields.DEFAULT.Purge(this);
		}

        public override System.Collections.Generic.ICollection<string> GetFieldNames(IndexReader.FieldOption fieldNames)
		{
			EnsureOpen();
            ISet<string> fieldSet = Lucene.Net.Support.Compatibility.SetFactory.CreateHashSet<string>();
			foreach(var reader in readers)
			{
				ICollection<string> names = reader.GetFieldNames(fieldNames);
                fieldSet.UnionWith(names);
			}
			return fieldSet;
		}
		
		private class ParallelTermEnum : TermEnum
		{
			private void  InitBlock(ParallelReader enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			private ParallelReader enclosingInstance;
			public ParallelReader Enclosing_Instance
			{
				get
				{
					return enclosingInstance;
				}
				
			}
			private System.String field;
			private IEnumerator<string> fieldIterator;
			private TermEnum termEnum;

		    private bool isDisposed;
			
			public ParallelTermEnum(ParallelReader enclosingInstance)
			{
				InitBlock(enclosingInstance);
				try
				{
					field = Enclosing_Instance.fieldToReader.Keys.First();
				}
				catch (ArgumentOutOfRangeException)
				{
					// No fields, so keep field == null, termEnum == null
					return;
				}
				if (field != null)
					termEnum = Enclosing_Instance.fieldToReader[field].Terms();
			}
			
			public ParallelTermEnum(ParallelReader enclosingInstance, Term term)
			{
				InitBlock(enclosingInstance);
				field = term.Field;
				IndexReader reader = Enclosing_Instance.fieldToReader[field];
				if (reader != null)
					termEnum = reader.Terms(term);
			}
			
			public override bool Next()
			{
				if (termEnum == null)
					return false;
				
				// another term in this field?
				if (termEnum.Next() && (System.Object) termEnum.Term.Field == (System.Object) field)
					return true; // yes, keep going
				
				termEnum.Close(); // close old termEnum
				
				// find the next field with terms, if any
				if (fieldIterator == null)
				{
                    var newList = new List<string>();  
                    if (Enclosing_Instance.fieldToReader != null && Enclosing_Instance.fieldToReader.Count > 0)
                    {
                        var comparer = Enclosing_Instance.fieldToReader.Comparer;
                        foreach(var entry in Enclosing_Instance.fieldToReader.Keys.Where(x => comparer.Compare(x, field) >= 0))
                            newList.Add(entry);
                    }

                    fieldIterator = newList.Skip(1).GetEnumerator(); // Skip field to get next one
				}
				while (fieldIterator.MoveNext())
				{
					field = fieldIterator.Current;
					termEnum = Enclosing_Instance.fieldToReader[field].Terms(new Term(field));
					Term term = termEnum.Term;
					if (term != null && (System.Object) term.Field == (System.Object) field)
						return true;
					else
						termEnum.Close();
				}
				
				return false; // no more fields
			}

		    public override Term Term
		    {
		        get
		        {
		            if (termEnum == null)
		                return null;

		            return termEnum.Term;
		        }
		    }

		    public override int DocFreq()
			{
				if (termEnum == null)
					return 0;
				
				return termEnum.DocFreq();
			}

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

                if (disposing)
                {
                    if (termEnum != null)
                        termEnum.Close();
                }

                isDisposed = true;
            }
		}
		
		// wrap a TermDocs in order to support seek(Term)
		private class ParallelTermDocs : TermDocs
		{
			private void  InitBlock(ParallelReader enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			private ParallelReader enclosingInstance;
			public ParallelReader Enclosing_Instance
			{
				get
				{
					return enclosingInstance;
				}
				
			}
			protected internal TermDocs termDocs;

		    private bool isDisposed;
			
			public ParallelTermDocs(ParallelReader enclosingInstance)
			{
				InitBlock(enclosingInstance);
			}
			public ParallelTermDocs(ParallelReader enclosingInstance, Term term)
			{
				InitBlock(enclosingInstance);
                if(term == null)
                    termDocs = (Enclosing_Instance.readers.Count == 0)
                                   ? null
                                   : Enclosing_Instance.readers[0].TermDocs(null);
                else
                    Seek(term);
			}

		    public virtual int Doc
		    {
		        get { return termDocs.Doc; }
		    }

		    public virtual int Freq
		    {
		        get { return termDocs.Freq; }
		    }

		    public virtual void  Seek(Term term)
			{
				IndexReader reader = Enclosing_Instance.fieldToReader[term.Field];
				termDocs = reader != null?reader.TermDocs(term):null;
			}
			
			public virtual void  Seek(TermEnum termEnum)
			{
				Seek(termEnum.Term);
			}
			
			public virtual bool Next()
			{
				if (termDocs == null)
					return false;
				
				return termDocs.Next();
			}
			
			public virtual int Read(int[] docs, int[] freqs)
			{
				if (termDocs == null)
					return 0;
				
				return termDocs.Read(docs, freqs);
			}
			
			public virtual bool SkipTo(int target)
			{
				if (termDocs == null)
					return false;
				
				return termDocs.SkipTo(target);
			}

            [Obsolete("Use Dispose() instead")]
            public virtual void Close()
            {
                Dispose();
            }

		    public void Dispose()
		    {
		        Dispose(true);
		    }

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

                if (disposing)
                {
                    if (termDocs != null)
                        termDocs.Close();
                }

                isDisposed = true;
            }
		}
		
		private class ParallelTermPositions:ParallelTermDocs, TermPositions
		{
			private void  InitBlock(ParallelReader enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			private ParallelReader enclosingInstance;
			public new ParallelReader Enclosing_Instance
			{
				get
				{
					return enclosingInstance;
				}
				
			}
			
			public ParallelTermPositions(ParallelReader enclosingInstance):base(enclosingInstance)
			{
				InitBlock(enclosingInstance);
			}
			public ParallelTermPositions(ParallelReader enclosingInstance, Term term):base(enclosingInstance)
			{
				InitBlock(enclosingInstance);
				Seek(term);
			}
			
			public override void  Seek(Term term)
			{
				IndexReader reader = Enclosing_Instance.fieldToReader[term.Field];
				termDocs = reader != null?reader.TermPositions(term):null;
			}
			
			public virtual int NextPosition()
			{
				// It is an error to call this if there is no next position, e.g. if termDocs==null
				return ((TermPositions) termDocs).NextPosition();
			}

		    public virtual int PayloadLength
		    {
		        get { return ((TermPositions) termDocs).PayloadLength; }
		    }

		    public virtual byte[] GetPayload(byte[] data, int offset)
			{
				return ((TermPositions) termDocs).GetPayload(data, offset);
			}
			
			
			// TODO: Remove warning after API has been finalized

		    public virtual bool IsPayloadAvailable
		    {
		        get { return ((TermPositions) termDocs).IsPayloadAvailable; }
		    }
		}
	}
}