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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.store.Directory;

/**
 * An {@link IndexDeletionPolicy} that wraps any other {@link IndexDeletionPolicy} and adds the
 * ability to hold and later release snapshots of an index. While a snapshot is held, the {@link
 * IndexWriter} will not remove any files associated with it even if the index is otherwise being
 * actively, arbitrarily changed. Because we wrap another arbitrary {@link IndexDeletionPolicy},
 * this gives you the freedom to continue using whatever {@link IndexDeletionPolicy} you would
 * normally want to use with your index.
 *
 * <p>This class maintains all snapshots in-memory, and so the information is not persisted and not
 * protected against system failures. If persistence is important, you can use {@link
 * PersistentSnapshotDeletionPolicy}.
 *
 * @lucene.experimental
 */
public class SnapshotDeletionPolicy extends IndexDeletionPolicy {

  /** Records how many snapshots are held against each commit generation */
  protected final Map<Long, Integer> refCounts = new HashMap<>();

  /** Used to map gen to IndexCommit. */
  protected final Map<Long, IndexCommit> indexCommits = new HashMap<>();

  /** Wrapped {@link IndexDeletionPolicy} */
  private final IndexDeletionPolicy primary;

  /** Most recently committed {@link IndexCommit}. */
  protected IndexCommit lastCommit;

  /** Used to detect misuse */
  private boolean initCalled;

  /** Sole constructor, taking the incoming {@link IndexDeletionPolicy} to wrap. */
  public SnapshotDeletionPolicy(IndexDeletionPolicy primary) {
    this.primary = primary;
  }

  @Override
  public synchronized void onCommit(List<? extends IndexCommit> commits) throws IOException {
    primary.onCommit(wrapCommits(commits));
    lastCommit = commits.get(commits.size() - 1);
  }

  @Override
  public synchronized void onInit(List<? extends IndexCommit> commits) throws IOException {
    initCalled = true;
    primary.onInit(wrapCommits(commits));
    for (IndexCommit commit : commits) {
      if (refCounts.containsKey(commit.getGeneration())) {
        indexCommits.put(commit.getGeneration(), commit);
      }
    }
    if (!commits.isEmpty()) {
      lastCommit = commits.get(commits.size() - 1);
    }
  }

  /**
   * Release a snapshotted commit.
   *
   * @param commit the commit previously returned by {@link #snapshot}
   */
  public synchronized void release(IndexCommit commit) throws IOException {
    long gen = commit.getGeneration();
    releaseGen(gen);
  }

  /** Release a snapshot by generation. */
  protected void releaseGen(long gen) throws IOException {
    if (!initCalled) {
      throw new IllegalStateException(
          "this instance is not being used by IndexWriter; be sure to use the instance returned from writer.getConfig().getIndexDeletionPolicy()");
    }
    Integer refCount = refCounts.get(gen);
    if (refCount == null) {
      throw new IllegalArgumentException("commit gen=" + gen + " is not currently snapshotted");
    }
    int refCountInt = refCount.intValue();
    assert refCountInt > 0;
    refCountInt--;
    if (refCountInt == 0) {
      refCounts.remove(gen);
      indexCommits.remove(gen);
    } else {
      refCounts.put(gen, refCountInt);
    }
  }

  /** Increments the refCount for this {@link IndexCommit}. */
  protected synchronized void incRef(IndexCommit ic) {
    long gen = ic.getGeneration();
    Integer refCount = refCounts.get(gen);
    int refCountInt;
    if (refCount == null) {
      indexCommits.put(gen, lastCommit);
      refCountInt = 0;
    } else {
      refCountInt = refCount.intValue();
    }
    refCounts.put(gen, refCountInt + 1);
  }

  /**
   * Snapshots the last commit and returns it. Once a commit is 'snapshotted,' it is protected from
   * deletion (as long as this {@link IndexDeletionPolicy} is used). The snapshot can be removed by
   * calling {@link #release(IndexCommit)} followed by a call to {@link
   * IndexWriter#deleteUnusedFiles()}.
   *
   * <p><b>NOTE:</b> while the snapshot is held, the files it references will not be deleted, which
   * will consume additional disk space in your index. If you take a snapshot at a particularly bad
   * time (say just before you call forceMerge) then in the worst case this could consume an extra
   * 1X of your total index size, until you release the snapshot.
   *
   * @throws IllegalStateException if this index does not have any commits yet
   * @return the {@link IndexCommit} that was snapshotted.
   */
  public synchronized IndexCommit snapshot() throws IOException {
    if (!initCalled) {
      throw new IllegalStateException(
          "this instance is not being used by IndexWriter; be sure to use the instance returned from writer.getConfig().getIndexDeletionPolicy()");
    }
    if (lastCommit == null) {
      // No commit yet, eg this is a new IndexWriter:
      throw new IllegalStateException("No index commit to snapshot");
    }

    incRef(lastCommit);

    return lastCommit;
  }

  /** Returns all IndexCommits held by at least one snapshot. */
  public synchronized List<IndexCommit> getSnapshots() {
    return new ArrayList<>(indexCommits.values());
  }

  /** Returns the total number of snapshots currently held. */
  public synchronized int getSnapshotCount() {
    int total = 0;
    for (Integer refCount : refCounts.values()) {
      total += refCount.intValue();
    }

    return total;
  }

  /**
   * Retrieve an {@link IndexCommit} from its generation; returns null if this IndexCommit is not
   * currently snapshotted
   */
  public synchronized IndexCommit getIndexCommit(long gen) {
    return indexCommits.get(gen);
  }

  /** Wraps each {@link IndexCommit} as a {@link SnapshotCommitPoint}. */
  private List<IndexCommit> wrapCommits(List<? extends IndexCommit> commits) {
    List<IndexCommit> wrappedCommits = new ArrayList<>(commits.size());
    for (IndexCommit ic : commits) {
      wrappedCommits.add(new SnapshotCommitPoint(ic));
    }
    return wrappedCommits;
  }

  /** Wraps a provided {@link IndexCommit} and prevents it from being deleted. */
  private class SnapshotCommitPoint extends IndexCommit {

    /** The {@link IndexCommit} we are preventing from deletion. */
    protected IndexCommit cp;

    /** Creates a {@code SnapshotCommitPoint} wrapping the provided {@link IndexCommit}. */
    protected SnapshotCommitPoint(IndexCommit cp) {
      this.cp = cp;
    }

    @Override
    public String toString() {
      return "SnapshotDeletionPolicy.SnapshotCommitPoint(" + cp + ")";
    }

    @Override
    public void delete() {
      synchronized (SnapshotDeletionPolicy.this) {
        // Suppress the delete request if this commit point is
        // currently snapshotted.
        if (!refCounts.containsKey(cp.getGeneration())) {
          cp.delete();
        }
      }
    }

    @Override
    public Directory getDirectory() {
      return cp.getDirectory();
    }

    @Override
    public Collection<String> getFileNames() throws IOException {
      return cp.getFileNames();
    }

    @Override
    public long getGeneration() {
      return cp.getGeneration();
    }

    @Override
    public String getSegmentsFileName() {
      return cp.getSegmentsFileName();
    }

    @Override
    public Map<String, String> getUserData() throws IOException {
      return cp.getUserData();
    }

    @Override
    public boolean isDeleted() {
      return cp.isDeleted();
    }

    @Override
    public int getSegmentCount() {
      return cp.getSegmentCount();
    }
  }
}
