/*
 * 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.Closeable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.util.ThreadInterruptedException;

/**
 * Utility class that runs a thread to manage periodicc reopens of a {@link ReferenceManager}, with
 * methods to wait for a specific index changes to become visible. When a given search request needs
 * to see a specific index change, call the {#waitForGeneration} to wait for that change to be
 * visible. Note that this will only scale well if most searches do not need to wait for a specific
 * index generation.
 *
 * @lucene.experimental
 */
public class ControlledRealTimeReopenThread<T> extends Thread implements Closeable {
  private final ReferenceManager<T> manager;
  private final long targetMaxStaleNS;
  private final long targetMinStaleNS;
  private final IndexWriter writer;
  private volatile boolean finish;
  private volatile long waitingGen;
  private volatile long searchingGen;
  private long refreshStartGen;

  private final ReentrantLock reopenLock = new ReentrantLock();
  private final Condition reopenCond = reopenLock.newCondition();

  /**
   * Create ControlledRealTimeReopenThread, to periodically reopen the {@link ReferenceManager}.
   *
   * @param targetMaxStaleSec Maximum time until a new reader must be opened; this sets the upper
   *     bound on how slowly reopens may occur, when no caller is waiting for a specific generation
   *     to become visible.
   * @param targetMinStaleSec Mininum time until a new reader can be opened; this sets the lower
   *     bound on how quickly reopens may occur, when a caller is waiting for a specific generation
   *     to become visible.
   */
  public ControlledRealTimeReopenThread(
      IndexWriter writer,
      ReferenceManager<T> manager,
      double targetMaxStaleSec,
      double targetMinStaleSec) {
    if (targetMaxStaleSec < targetMinStaleSec) {
      throw new IllegalArgumentException(
          "targetMaxScaleSec (= "
              + targetMaxStaleSec
              + ") < targetMinStaleSec (="
              + targetMinStaleSec
              + ")");
    }
    this.writer = writer;
    this.manager = manager;
    this.targetMaxStaleNS = (long) (1000000000 * targetMaxStaleSec);
    this.targetMinStaleNS = (long) (1000000000 * targetMinStaleSec);
    manager.addListener(new HandleRefresh());
  }

  private class HandleRefresh implements ReferenceManager.RefreshListener {
    @Override
    public void beforeRefresh() {
      // Save the gen as of when we started the reopen; the
      // listener (HandleRefresh above) copies this to
      // searchingGen once the reopen completes:
      refreshStartGen = writer.getMaxCompletedSequenceNumber();
    }

    @Override
    public void afterRefresh(boolean didRefresh) {
      synchronized (ControlledRealTimeReopenThread.this) {
        searchingGen = refreshStartGen;
        ControlledRealTimeReopenThread.this.notifyAll();
      }
    }
  }

  @Override
  public synchronized void close() {
    // System.out.println("NRT: set finish");

    finish = true;

    // So thread wakes up and notices it should finish:
    reopenLock.lock();
    try {
      reopenCond.signal();
    } finally {
      reopenLock.unlock();
    }

    try {
      join();
    } catch (InterruptedException ie) {
      throw new ThreadInterruptedException(ie);
    }

    // Max it out so any waiting search threads will return:
    searchingGen = Long.MAX_VALUE;
    notifyAll();
  }

  /**
   * Waits for the target generation to become visible in the searcher. If the current searcher is
   * older than the target generation, this method will block until the searcher is reopened, by
   * another via {@link ReferenceManager#maybeRefresh} or until the {@link ReferenceManager} is
   * closed.
   *
   * @param targetGen the generation to wait for
   */
  public void waitForGeneration(long targetGen) throws InterruptedException {
    waitForGeneration(targetGen, -1);
  }

  /**
   * Waits for the target generation to become visible in the searcher, up to a maximum specified
   * milli-seconds. If the current searcher is older than the target generation, this method will
   * block until the searcher has been reopened by another thread via {@link
   * ReferenceManager#maybeRefresh}, the given waiting time has elapsed, or until the {@link
   * ReferenceManager} is closed.
   *
   * <p>NOTE: if the waiting time elapses before the requested target generation is available the
   * current {@link SearcherManager} is returned instead.
   *
   * @param targetGen the generation to wait for
   * @param maxMS maximum milliseconds to wait, or -1 to wait indefinitely
   * @return true if the targetGeneration is now available, or false if maxMS wait time was exceeded
   */
  public synchronized boolean waitForGeneration(long targetGen, int maxMS)
      throws InterruptedException {
    if (targetGen > searchingGen) {
      // Notify the reopen thread that the waitingGen has
      // changed, so it may wake up and realize it should
      // not sleep for much or any longer before reopening:
      reopenLock.lock();

      // Need to find waitingGen inside lock as it's used to determine
      // stale time
      waitingGen = Math.max(waitingGen, targetGen);

      try {
        reopenCond.signal();
      } finally {
        reopenLock.unlock();
      }

      long startMS = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());

      while (targetGen > searchingGen) {
        if (maxMS < 0) {
          wait();
        } else {
          long msLeft = (startMS + maxMS) - TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
          if (msLeft <= 0) {
            return false;
          } else {
            wait(msLeft);
          }
        }
      }
    }

    return true;
  }

  @Override
  public void run() {
    // TODO: maybe use private thread ticktock timer, in
    // case clock shift messes up nanoTime?
    long lastReopenStartNS = System.nanoTime();

    // System.out.println("reopen: start");
    while (!finish) {

      // TODO: try to guestimate how long reopen might
      // take based on past data?

      // Loop until we've waiting long enough before the
      // next reopen:
      while (!finish) {

        // Need lock before finding out if has waiting
        reopenLock.lock();
        try {
          // True if we have someone waiting for reopened searcher:
          boolean hasWaiting = waitingGen > searchingGen;
          final long nextReopenStartNS =
              lastReopenStartNS + (hasWaiting ? targetMinStaleNS : targetMaxStaleNS);

          final long sleepNS = nextReopenStartNS - System.nanoTime();

          if (sleepNS > 0) {
            reopenCond.awaitNanos(sleepNS);
          } else {
            break;
          }
        } catch (
            @SuppressWarnings("unused")
            InterruptedException ie) {
          Thread.currentThread().interrupt();
          return;
        } finally {
          reopenLock.unlock();
        }
      }

      if (finish) {
        break;
      }

      lastReopenStartNS = System.nanoTime();
      try {
        manager.maybeRefreshBlocking();
      } catch (IOException ioe) {
        throw new RuntimeException(ioe);
      }
    }
  }

  /** Returns which {@code generation} the current searcher is guaranteed to include. */
  public long getSearchingGen() {
    return searchingGen;
  }
}
