/* 
 * 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 NUnit.Framework;

using SimpleAnalyzer = Lucene.Net.Analysis.SimpleAnalyzer;
using Document = Lucene.Net.Documents.Document;
using Field = Lucene.Net.Documents.Field;
using Norm = Lucene.Net.Index.SegmentReader.Norm;
using AlreadyClosedException = Lucene.Net.Store.AlreadyClosedException;
using Directory = Lucene.Net.Store.Directory;
using FSDirectory = Lucene.Net.Store.FSDirectory;
using LockObtainFailedException = Lucene.Net.Store.LockObtainFailedException;
using MockRAMDirectory = Lucene.Net.Store.MockRAMDirectory;
using Similarity = Lucene.Net.Search.Similarity;
using LuceneTestCase = Lucene.Net.Util.LuceneTestCase;

namespace Lucene.Net.Index
{
	
	/// <summary> Tests cloning multiple types of readers, modifying the deletedDocs and norms
	/// and verifies copy on write semantics of the deletedDocs and norms is
	/// implemented properly
	/// </summary>
    [TestFixture]
	public class TestIndexReaderClone:LuceneTestCase
	{
		
        [Test]
		public virtual void  TestCloneReadOnlySegmentReader()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, false);
			IndexReader reader = IndexReader.Open(dir1, false);
			IndexReader readOnlyReader = reader.Clone(true);

            Assert.IsTrue(IsReadOnly(readOnlyReader), "reader isn't read only");
            Assert.IsFalse(DeleteWorked(1, readOnlyReader), "deleting from the original should not have worked");

			reader.Close();
			readOnlyReader.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1, clone to non-readOnly
		// reader2, make sure we can change reader2
        [Test]
		public virtual void  TestCloneNoChangesStillReadOnly()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader r1 = IndexReader.Open(dir1, false);
			IndexReader r2 = r1.Clone(false);

            Assert.IsTrue(DeleteWorked(1, r2), "deleting from the cloned should have worked");

			r1.Close();
			r2.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1, clone to non-readOnly
		// reader2, make sure we can change reader1
        [Test]
		public virtual void  TestCloneWriteToOrig()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader r1 = IndexReader.Open(dir1, false);
			IndexReader r2 = r1.Clone(false);

            Assert.IsTrue(DeleteWorked(1, r1), "deleting from the original should have worked");

			r1.Close();
			r2.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1, clone to non-readOnly
		// reader2, make sure we can change reader2
        [Test]
		public virtual void  TestCloneWriteToClone()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader r1 = IndexReader.Open(dir1, false);
			IndexReader r2 = r1.Clone(false);

            Assert.IsTrue(DeleteWorked(1, r2), "deleting from the original should have worked");

			// should fail because reader1 holds the write lock
			Assert.IsTrue(!DeleteWorked(1, r1), "first reader should not be able to delete");
			r2.Close();
			// should fail because we are now stale (reader1
			// committed changes)
			Assert.IsTrue(!DeleteWorked(1, r1), "first reader should not be able to delete");
			r1.Close();
			
			dir1.Close();
		}
		
		// create single-segment index, open non-readOnly
		// SegmentReader, add docs, reopen to multireader, then do
		// delete
        [Test]
		public virtual void  TestReopenSegmentReaderToMultiReader()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, false);
			IndexReader reader1 = IndexReader.Open(dir1, false);
			
			TestIndexReaderReopen.ModifyIndex(5, dir1);
			
			IndexReader reader2 = reader1.Reopen();
			Assert.IsTrue(reader1 != reader2);
			
			Assert.IsTrue(DeleteWorked(1, reader2));
			reader1.Close();
			reader2.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1, clone to readOnly reader2
        [Test]
		public virtual void  TestCloneWriteableToReadOnly()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader = IndexReader.Open(dir1, false);
			IndexReader readOnlyReader = reader.Clone(true);

            Assert.IsTrue(IsReadOnly(readOnlyReader), "reader isn't read only");
            Assert.IsFalse(DeleteWorked(1, readOnlyReader), "deleting from the original should not have worked");

            // this readonly reader shouldn't have a write lock
            Assert.IsFalse(readOnlyReader.hasChanges, "readOnlyReader has a write lock");

			reader.Close();
			readOnlyReader.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1, reopen to readOnly reader2
        [Test]
		public virtual void  TestReopenWriteableToReadOnly()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader = IndexReader.Open(dir1, false);
			int docCount = reader.NumDocs();
			Assert.IsTrue(DeleteWorked(1, reader));
			Assert.AreEqual(docCount - 1, reader.NumDocs());
			
			IndexReader readOnlyReader = reader.Reopen(true);
            Assert.IsTrue(IsReadOnly(readOnlyReader), "reader isn't read only");

			Assert.IsFalse(DeleteWorked(1, readOnlyReader));
			Assert.AreEqual(docCount - 1, readOnlyReader.NumDocs());
			reader.Close();
			readOnlyReader.Close();
			dir1.Close();
		}
		
		// open readOnly reader1, clone to non-readOnly reader2
        [Test]
		public virtual void  TestCloneReadOnlyToWriteable()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader1 = IndexReader.Open(dir1, true);
			
			IndexReader reader2 = reader1.Clone(false);

            Assert.IsFalse(IsReadOnly(reader2), "reader should not be read only");

			Assert.IsFalse(DeleteWorked(1, reader1), "deleting from the original reader should not have worked");
			// this readonly reader shouldn't yet have a write lock
            Assert.IsFalse(reader2.hasChanges, "cloned reader should not have write lock");

			Assert.IsTrue(DeleteWorked(1, reader2), "deleting from the cloned reader should have worked");
			reader1.Close();
			reader2.Close();
			dir1.Close();
		}
		
		// open non-readOnly reader1 on multi-segment index, then
		// optimize the index, then clone to readOnly reader2
        [Test]
		public virtual void  TestReadOnlyCloneAfterOptimize()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader1 = IndexReader.Open(dir1, false);
			IndexWriter w = new IndexWriter(dir1, new SimpleAnalyzer(), IndexWriter.MaxFieldLength.LIMITED);
			w.Optimize();
			w.Close();
			IndexReader reader2 = reader1.Clone(true);
			Assert.IsTrue(IsReadOnly(reader2));
			reader1.Close();
			reader2.Close();
			dir1.Close();
		}
		
		private static bool DeleteWorked(int doc, IndexReader r)
		{
			bool exception = false;
			try
			{
				// trying to delete from the original reader should throw an exception
				r.DeleteDocument(doc);
			}
			catch (System.Exception ex)
			{
				exception = true;
			}
			return !exception;
		}
		
        [Test]
		public virtual void  TestCloneReadOnlyDirectoryReader()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader = IndexReader.Open(dir1, false);
			IndexReader readOnlyReader = reader.Clone(true);

            Assert.IsTrue(IsReadOnly(readOnlyReader), "reader isn't read only");

			reader.Close();
			readOnlyReader.Close();
			dir1.Close();
		}
		
		public static bool IsReadOnly(IndexReader r)
		{
			if (r is ReadOnlySegmentReader || r is ReadOnlyDirectoryReader)
				return true;
			return false;
		}
		
        [Test]
		public virtual void  TestParallelReader()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, true);
			Directory dir2 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir2, true);
			IndexReader r1 = IndexReader.Open(dir1, false);
            IndexReader r2 = IndexReader.Open(dir2, false);
			
			ParallelReader pr1 = new ParallelReader();
			pr1.Add(r1);
			pr1.Add(r2);
			
			PerformDefaultTests(pr1);
			pr1.Close();
			dir1.Close();
			dir2.Close();
		}
		
		/// <summary> 1. Get a norm from the original reader 2. Clone the original reader 3.
		/// Delete a document and set the norm of the cloned reader 4. Verify the norms
		/// are not the same on each reader 5. Verify the doc deleted is only in the
		/// cloned reader 6. Try to delete a document in the original reader, an
		/// exception should be thrown
		/// 
		/// </summary>
		/// <param name="r1">IndexReader to perform tests on
		/// </param>
		/// <throws>  Exception </throws>
		private void  PerformDefaultTests(IndexReader r1)
		{
			float norm1 = Similarity.DecodeNorm(r1.Norms("field1")[4]);
			
			IndexReader pr1Clone = (IndexReader) r1.Clone();
			pr1Clone.DeleteDocument(10);
			pr1Clone.SetNorm(4, "field1", 0.5f);
			Assert.IsTrue(Similarity.DecodeNorm(r1.Norms("field1")[4]) == norm1);
			Assert.IsTrue(Similarity.DecodeNorm(pr1Clone.Norms("field1")[4]) != norm1);
			
			Assert.IsTrue(!r1.IsDeleted(10));
			Assert.IsTrue(pr1Clone.IsDeleted(10));
			
			// try to update the original reader, which should throw an exception
            Assert.Throws<LockObtainFailedException>(() => r1.DeleteDocument(11),
		                             "Tried to delete doc 11 and an exception should have been thrown");
			pr1Clone.Close();
		}
		
        [Test]
		public virtual void  TestMixedReaders()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, true);
			Directory dir2 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir2, true);
            IndexReader r1 = IndexReader.Open(dir1, false);
            IndexReader r2 = IndexReader.Open(dir2, false);
			
			MultiReader multiReader = new MultiReader(new IndexReader[]{r1, r2});
			PerformDefaultTests(multiReader);
			multiReader.Close();
			dir1.Close();
			dir2.Close();
		}
		
        [Test]
		public virtual void  TestSegmentReaderUndeleteall()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			SegmentReader origSegmentReader = SegmentReader.GetOnlySegmentReader(dir1);
			origSegmentReader.DeleteDocument(10);
			AssertDelDocsRefCountEquals(1, origSegmentReader);
			origSegmentReader.UndeleteAll();
			Assert.IsNull(origSegmentReader.deletedDocsRef_ForNUnit);
			origSegmentReader.Close();
			// need to test norms?
			dir1.Close();
		}
		
        [Test]
		public virtual void  TestSegmentReaderCloseReferencing()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			SegmentReader origSegmentReader = SegmentReader.GetOnlySegmentReader(dir1);
			origSegmentReader.DeleteDocument(1);
			origSegmentReader.SetNorm(4, "field1", 0.5f);
			
			SegmentReader clonedSegmentReader = (SegmentReader) origSegmentReader.Clone();
			AssertDelDocsRefCountEquals(2, origSegmentReader);
			origSegmentReader.Close();
			AssertDelDocsRefCountEquals(1, origSegmentReader);
			// check the norm refs
			Norm norm = clonedSegmentReader.norms_ForNUnit["field1"];
			Assert.AreEqual(1, norm.BytesRef().RefCount());
			clonedSegmentReader.Close();
			dir1.Close();
		}
		
        [Test]
		public virtual void  TestSegmentReaderDelDocsReferenceCounting()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			
			IndexReader origReader = IndexReader.Open(dir1, false);
			SegmentReader origSegmentReader = SegmentReader.GetOnlySegmentReader(origReader);
			// deletedDocsRef should be null because nothing has updated yet
			Assert.IsNull(origSegmentReader.deletedDocsRef_ForNUnit);
			
			// we deleted a document, so there is now a deletedDocs bitvector and a
			// reference to it
			origReader.DeleteDocument(1);
			AssertDelDocsRefCountEquals(1, origSegmentReader);
			
			// the cloned segmentreader should have 2 references, 1 to itself, and 1 to
			// the original segmentreader
			IndexReader clonedReader = (IndexReader) origReader.Clone();
			SegmentReader clonedSegmentReader = SegmentReader.GetOnlySegmentReader(clonedReader);
			AssertDelDocsRefCountEquals(2, origSegmentReader);
			// deleting a document creates a new deletedDocs bitvector, the refs goes to
			// 1
			clonedReader.DeleteDocument(2);
			AssertDelDocsRefCountEquals(1, origSegmentReader);
			AssertDelDocsRefCountEquals(1, clonedSegmentReader);
			
			// make sure the deletedocs objects are different (copy
			// on write)
			Assert.IsTrue(origSegmentReader.deletedDocs_ForNUnit != clonedSegmentReader.deletedDocs_ForNUnit);
			
			AssertDocDeleted(origSegmentReader, clonedSegmentReader, 1);
			Assert.IsTrue(!origSegmentReader.IsDeleted(2)); // doc 2 should not be deleted
			// in original segmentreader
			Assert.IsTrue(clonedSegmentReader.IsDeleted(2)); // doc 2 should be deleted in
			// cloned segmentreader
			
			// deleting a doc from the original segmentreader should throw an exception
            Assert.Throws<LockObtainFailedException>(() => origReader.DeleteDocument(4), "expected exception");
			
			origReader.Close();
			// try closing the original segment reader to see if it affects the
			// clonedSegmentReader
			clonedReader.DeleteDocument(3);
			clonedReader.Flush();
			AssertDelDocsRefCountEquals(1, clonedSegmentReader);
			
			// test a reopened reader
			IndexReader reopenedReader = clonedReader.Reopen();
			IndexReader cloneReader2 = (IndexReader) reopenedReader.Clone();
			SegmentReader cloneSegmentReader2 = SegmentReader.GetOnlySegmentReader(cloneReader2);
			AssertDelDocsRefCountEquals(2, cloneSegmentReader2);
			clonedReader.Close();
			reopenedReader.Close();
			cloneReader2.Close();
			
			dir1.Close();
		}
		
		// LUCENE-1648
        [Test]
		public virtual void  TestCloneWithDeletes()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			IndexReader origReader = IndexReader.Open(dir1, false);
			origReader.DeleteDocument(1);
			
			IndexReader clonedReader = (IndexReader) origReader.Clone();
			origReader.Close();
			clonedReader.Close();
			
			IndexReader r = IndexReader.Open(dir1, false);
			Assert.IsTrue(r.IsDeleted(1));
			r.Close();
			dir1.Close();
		}
		
		// LUCENE-1648
        [Test]
		public virtual void  TestCloneWithSetNorm()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			IndexReader orig = IndexReader.Open(dir1, false);
			orig.SetNorm(1, "field1", 17.0f);
			byte encoded = Similarity.EncodeNorm(17.0f);
			Assert.AreEqual(encoded, orig.Norms("field1")[1]);
			
			// the cloned segmentreader should have 2 references, 1 to itself, and 1 to
			// the original segmentreader
			IndexReader clonedReader = (IndexReader) orig.Clone();
			orig.Close();
			clonedReader.Close();
			
			IndexReader r = IndexReader.Open(dir1, false);
			Assert.AreEqual(encoded, r.Norms("field1")[1]);
			r.Close();
			dir1.Close();
		}
		
		private void  AssertDocDeleted(SegmentReader reader, SegmentReader reader2, int doc)
		{
			Assert.AreEqual(reader.IsDeleted(doc), reader2.IsDeleted(doc));
		}
		
		private void  AssertDelDocsRefCountEquals(int refCount, SegmentReader reader)
		{
			Assert.AreEqual(refCount, reader.deletedDocsRef_ForNUnit.RefCount());
		}
		
        [Test]
		public virtual void  TestCloneSubreaders()
		{
			Directory dir1 = new MockRAMDirectory();
			
			TestIndexReaderReopen.CreateIndex(dir1, true);
			IndexReader reader = IndexReader.Open(dir1, false);
			reader.DeleteDocument(1); // acquire write lock
			IndexReader[] subs = reader.GetSequentialSubReaders();
			System.Diagnostics.Debug.Assert(subs.Length > 1);
			
			IndexReader[] clones = new IndexReader[subs.Length];
			for (int x = 0; x < subs.Length; x++)
			{
				clones[x] = (IndexReader) subs[x].Clone();
			}
			reader.Close();
			for (int x = 0; x < subs.Length; x++)
			{
				clones[x].Close();
			}
			dir1.Close();
		}
		
        [Test]
		public virtual void  TestLucene1516Bug()
		{
			Directory dir1 = new MockRAMDirectory();
			TestIndexReaderReopen.CreateIndex(dir1, false);
			IndexReader r1 = IndexReader.Open(dir1, false);
			r1.IncRef();
			IndexReader r2 = r1.Clone(false);
			r1.DeleteDocument(5);
			r1.DecRef();
			
			r1.IncRef();
			
			r2.Close();
			r1.DecRef();
			r1.Close();
			dir1.Close();
		}
		
        [Test]
		public virtual void  TestCloseStoredFields()
		{
			Directory dir = new MockRAMDirectory();
			IndexWriter w = new IndexWriter(dir, new SimpleAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
			w.UseCompoundFile = false;
			Document doc = new Document();
			doc.Add(new Field("field", "yes it's stored", Field.Store.YES, Field.Index.ANALYZED));
			w.AddDocument(doc);
			w.Close();
			IndexReader r1 = IndexReader.Open(dir, false);
			IndexReader r2 = r1.Clone(false);
			r1.Close();
			r2.Close();
			dir.Close();
		}
	}
}