package org.apache.lucene.index;

/**
 * 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.
 */
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.LuceneTestCase;

public class TestIndexWriterNRTIsCurrent extends LuceneTestCase {

  public static class ReaderHolder {
    volatile IndexReader reader;
    volatile boolean stop = false;
  }

  public void testIsCurrentWithThreads() throws CorruptIndexException,
      LockObtainFailedException, IOException, InterruptedException {
    Directory dir = newDirectory();
    IndexWriterConfig conf = newIndexWriterConfig(TEST_VERSION_CURRENT,
        new MockAnalyzer(random));
    IndexWriter writer = new IndexWriter(dir, conf);
    if (VERBOSE) {
      writer.setInfoStream(System.out);
    }
    ReaderHolder holder = new ReaderHolder();
    ReaderThread[] threads = new ReaderThread[atLeast(3)];
    final CountDownLatch latch = new CountDownLatch(1);
    WriterThread writerThread = new WriterThread(holder, writer,
        atLeast(500), random, latch);
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new ReaderThread(holder, latch);
      threads[i].start();
    }
    writerThread.start();

    writerThread.join();
    boolean failed = writerThread.failed != null;
    if (failed)
      writerThread.failed.printStackTrace();
    for (int i = 0; i < threads.length; i++) {
      threads[i].join();
      if (threads[i].failed != null) {
        threads[i].failed.printStackTrace();
        failed = true;
      }
    }
    assertFalse(failed);
    writer.close();
    dir.close();

  }

  public static class WriterThread extends Thread {
    private final ReaderHolder holder;
    private final IndexWriter writer;
    private final int numOps;
    private final Random random;
    private boolean countdown = true;
    private final CountDownLatch latch;
    Throwable failed;

    WriterThread(ReaderHolder holder, IndexWriter writer, int numOps,
        Random random, CountDownLatch latch) {
      super();
      this.holder = holder;
      this.writer = writer;
      this.numOps = numOps;
      this.random = random;
      this.latch = latch;
    }

    public void run() {
      IndexReader currentReader = null;
      try {
        Document doc = new Document();
        doc.add(new Field("id", "1", Field.Store.NO, Field.Index.ANALYZED));
        writer.addDocument(doc);
        holder.reader = currentReader = writer.getReader(true);
        Term term = new Term("id");
        for (int i = 0; i < numOps && !holder.stop; i++) {
          float nextOp = random.nextFloat();
          if (nextOp < 0.3) {
            term.set("id", "1");
            writer.updateDocument(term, doc);
          } else if (nextOp < 0.5) {
            writer.addDocument(doc);
          } else {
            term.set("id", "1");
            writer.deleteDocuments(term);
          }
          if (holder.reader != currentReader) {
            holder.reader = currentReader;
            if (countdown) {
              countdown = false;
              latch.countDown();
            }
          }
          if (random.nextBoolean()) {
            writer.commit();
            final IndexReader newReader = IndexReader
                .openIfChanged(currentReader);
            if (newReader != null) { 
              currentReader.decRef();
              currentReader = newReader;
            }
            if (currentReader.numDocs() == 0) {
              writer.addDocument(doc);
            }
          }
        }
      } catch (Throwable e) {
        failed = e;
      } finally {
        holder.reader = null;
        if (countdown) {
          latch.countDown();
        }
        if (currentReader != null) {
          try {
            currentReader.decRef();
          } catch (IOException e) {
          }
        }
      }
      if (VERBOSE) {
        System.out.println("writer stopped - forced by reader: " + holder.stop);
      }
    }
    
  }

  public static final class ReaderThread extends Thread {
    private final ReaderHolder holder;
    private final CountDownLatch latch;
    Throwable failed;

    ReaderThread(ReaderHolder holder, CountDownLatch latch) {
      super();
      this.holder = holder;
      this.latch = latch;
    }

    public void run() {
      try {
        latch.await();
      } catch (InterruptedException e) {
        failed = e;
        return;
      }
      IndexReader reader;
      while ((reader = holder.reader) != null) {
        if (reader.tryIncRef()) {
          try {
            boolean current = reader.isCurrent();
            if (VERBOSE) {
              System.out.println("Thread: " + Thread.currentThread() + " Reader: " + reader + " isCurrent:" + current);
            }

            assertFalse(current);
          } catch (Throwable e) {
            if (VERBOSE) {
              System.out.println("FAILED Thread: " + Thread.currentThread() + " Reader: " + reader + " isCurrent: false");
            }
            failed = e;
            holder.stop = true;
            return;
          } finally {
            try {
              reader.decRef();
            } catch (IOException e) {
              if (failed == null) {
                failed = e;
              }
              return;
            }
          }
        }
      }
    }
  }
}
