/*
 * 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.
 */
package org.apache.lucene.search;

import java.io.IOException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;

/**
 * Utility class to safely share {@link IndexSearcher} instances across multiple threads, while
 * periodically reopening. This class ensures each searcher is closed only once all threads have
 * finished using it.
 *
 * <p>Use {@link #acquire} to obtain the current searcher, and {@link #release} to release it, like
 * this:
 *
 * <pre class="prettyprint">
 * IndexSearcher s = manager.acquire();
 * try {
 *   // Do searching, doc retrieval, etc. with s
 * } finally {
 *   manager.release(s);
 * }
 * // Do not use s after this!
 * s = null;
 * </pre>
 *
 * <p>In addition you should periodically call {@link #maybeRefresh}. While it's possible to call
 * this just before running each query, this is discouraged since it penalizes the unlucky queries
 * that need to refresh. It's better to use a separate background thread, that periodically calls
 * {@link #maybeRefresh}. Finally, be sure to call {@link #close} once you are done.
 *
 * @see SearcherFactory
 * @lucene.experimental
 */
public final class SearcherManager extends ReferenceManager<IndexSearcher> {

  private final SearcherFactory searcherFactory;
  private RefreshCommitSupplier refreshCommitSupplier = new RefreshCommitSupplier() {};

  /**
   * Creates and returns a new SearcherManager from the given {@link IndexWriter}.
   *
   * @param writer the IndexWriter to open the IndexReader from.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(IndexWriter writer, SearcherFactory searcherFactory) throws IOException {
    this(writer, true, false, searcherFactory);
  }

  /**
   * Expert: creates and returns a new SearcherManager from the given {@link IndexWriter},
   * controlling whether past deletions should be applied.
   *
   * @param writer the IndexWriter to open the IndexReader from.
   * @param applyAllDeletes If <code>true</code>, all buffered deletes will be applied (made
   *     visible) in the {@link IndexSearcher} / {@link DirectoryReader}. If <code>false</code>, the
   *     deletes may or may not be applied, but remain buffered (in IndexWriter) so that they will
   *     be applied in the future. Applying deletes can be costly, so if your app can tolerate
   *     deleted documents being returned you might gain some performance by passing <code>false
   *     </code>. See {@link DirectoryReader#openIfChanged(DirectoryReader, IndexWriter, boolean)}.
   * @param writeAllDeletes If <code>true</code>, new deletes will be forcefully written to index
   *     files.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(
      IndexWriter writer,
      boolean applyAllDeletes,
      boolean writeAllDeletes,
      SearcherFactory searcherFactory)
      throws IOException {
    this(writer, applyAllDeletes, writeAllDeletes, searcherFactory, null);
  }

  /**
   * Expert: creates and returns a new SearcherManager from the given {@link IndexWriter},
   * controlling whether past deletions should be applied.
   *
   * @param writer the IndexWriter to open the IndexReader from.
   * @param applyAllDeletes If <code>true</code>, all buffered deletes will be applied (made
   *     visible) in the {@link IndexSearcher} / {@link DirectoryReader}. If <code>false</code>, the
   *     deletes may or may not be applied, but remain buffered (in IndexWriter) so that they will
   *     be applied in the future. Applying deletes can be costly, so if your app can tolerate
   *     deleted documents being returned you might gain some performance by passing <code>false
   *     </code>. See {@link DirectoryReader#openIfChanged(DirectoryReader, IndexWriter, boolean)}.
   * @param writeAllDeletes If <code>true</code>, new deletes will be forcefully written to index
   *     files.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @param refreshCommitSupplier supplier for providing the commit to refresh on
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(
      IndexWriter writer,
      boolean applyAllDeletes,
      boolean writeAllDeletes,
      SearcherFactory searcherFactory,
      RefreshCommitSupplier refreshCommitSupplier)
      throws IOException {
    if (searcherFactory == null) {
      searcherFactory = new SearcherFactory();
    }
    this.searcherFactory = searcherFactory;
    current =
        getSearcher(
            searcherFactory, DirectoryReader.open(writer, applyAllDeletes, writeAllDeletes), null);
    if (refreshCommitSupplier != null) {
      this.refreshCommitSupplier = refreshCommitSupplier;
    }
  }

  /**
   * Creates and returns a new SearcherManager from the given {@link Directory}.
   *
   * @param dir the directory to open the DirectoryReader on.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(Directory dir, SearcherFactory searcherFactory) throws IOException {
    if (searcherFactory == null) {
      searcherFactory = new SearcherFactory();
    }
    this.searcherFactory = searcherFactory;
    current = getSearcher(searcherFactory, DirectoryReader.open(dir), null);
  }

  /**
   * Creates and returns a new SearcherManager from an existing {@link DirectoryReader}. Note that
   * this steals the incoming reference.
   *
   * @param reader the DirectoryReader.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(DirectoryReader reader, SearcherFactory searcherFactory)
      throws IOException {
    this(reader, searcherFactory, null);
  }

  /**
   * Creates and returns a new SearcherManager from an existing {@link DirectoryReader}. Note that
   * this steals the incoming reference.
   *
   * @param reader the DirectoryReader.
   * @param searcherFactory An optional {@link SearcherFactory}. Pass <code>null</code> if you don't
   *     require the searcher to be warmed before going live or other custom behavior.
   * @param refreshCommitSupplier supplier for providing the commit to refresh on
   * @throws IOException if there is a low-level I/O error
   */
  public SearcherManager(
      DirectoryReader reader,
      SearcherFactory searcherFactory,
      RefreshCommitSupplier refreshCommitSupplier)
      throws IOException {
    if (searcherFactory == null) {
      searcherFactory = new SearcherFactory();
    }
    this.searcherFactory = searcherFactory;
    this.current = getSearcher(searcherFactory, reader, null);
    if (refreshCommitSupplier != null) {
      this.refreshCommitSupplier = refreshCommitSupplier;
    }
  }

  @Override
  protected void decRef(IndexSearcher reference) throws IOException {
    reference.getIndexReader().decRef();
  }

  @Override
  protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
    final IndexReader r = referenceToRefresh.getIndexReader();
    assert r instanceof DirectoryReader
        : "searcher's IndexReader should be a DirectoryReader, but got " + r;
    DirectoryReader dr = (DirectoryReader) r;
    IndexCommit refreshCommit = refreshCommitSupplier.getSearcherRefreshCommit(dr);
    final IndexReader newReader = DirectoryReader.openIfChanged(dr, refreshCommit);
    if (newReader == null) {
      return null;
    } else {
      return getSearcher(searcherFactory, newReader, r);
    }
  }

  @Override
  protected boolean tryIncRef(IndexSearcher reference) {
    return reference.getIndexReader().tryIncRef();
  }

  @Override
  protected int getRefCount(IndexSearcher reference) {
    return reference.getIndexReader().getRefCount();
  }

  /** Return index commit generation for current searcher. pkg-private for testing */
  long getSearcherCommitGeneration() throws IOException {
    IndexSearcher s = acquire();
    long gen = ((DirectoryReader) s.getIndexReader()).getIndexCommit().getGeneration();
    release(s);
    return gen;
  }

  /**
   * Returns <code>true</code> if no changes have occurred since this searcher ie. reader was
   * opened, otherwise <code>false</code>. pkg-private for testing
   *
   * @see DirectoryReader#isCurrent()
   */
  boolean isSearcherCurrent() throws IOException {
    final IndexSearcher searcher = acquire();
    try {
      final IndexReader r = searcher.getIndexReader();
      assert r instanceof DirectoryReader
          : "searcher's IndexReader should be a DirectoryReader, but got " + r;
      return ((DirectoryReader) r).isCurrent();
    } finally {
      release(searcher);
    }
  }

  /**
   * Expert: creates a searcher from the provided {@link IndexReader} using the provided {@link
   * SearcherFactory}. NOTE: this decRefs incoming reader on throwing an exception.
   */
  public static IndexSearcher getSearcher(
      SearcherFactory searcherFactory, IndexReader reader, IndexReader previousReader)
      throws IOException {
    boolean success = false;
    final IndexSearcher searcher;
    try {
      searcher = searcherFactory.newSearcher(reader, previousReader);
      if (searcher.getIndexReader() != reader) {
        throw new IllegalStateException(
            "SearcherFactory must wrap exactly the provided reader (got "
                + searcher.getIndexReader()
                + " but expected "
                + reader
                + ")");
      }
      success = true;
    } finally {
      if (!success) {
        reader.decRef();
      }
    }
    return searcher;
  }
}
