// Copyright (C) 2013 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.index;

import com.google.common.collect.ImmutableList;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import java.util.Optional;

/**
 * Secondary index implementation for arbitrary documents.
 *
 * <p>Documents are inserted into the index and are queried by converting special {@link
 * com.google.gerrit.index.query.Predicate} instances into index-aware predicates that use the index
 * search results as a source.
 *
 * <p>Implementations must be thread-safe and should batch inserts/updates where appropriate.
 */
public interface Index<K, V> {
  /** Returns the schema version used by this index. */
  Schema<V> getSchema();

  /** Close this index. */
  void close();

  /**
   * Insert a document into the index.
   *
   * <p>Results may not be immediately visible to searchers, but should be visible within a
   * reasonable amount of time.
   *
   * @param obj document object
   */
  void insert(V obj);

  /**
   * Update a document in the index.
   *
   * <p>Semantically equivalent to deleting the document and reinserting it with new field values. A
   * document that does not already exist is created. Results may not be immediately visible to
   * searchers, but should be visible within a reasonable amount of time.
   *
   * @param obj document object
   */
  void replace(V obj);

  /**
   * Delete a document from the index by key.
   *
   * @param key document key
   */
  void delete(K key);

  /** Delete all documents from the index. */
  void deleteAll();

  /**
   * Convert the given operator predicate into a source searching the index and returning only the
   * documents matching that predicate.
   *
   * <p>This method may be called multiple times for variations on the same predicate or multiple
   * predicate subtrees in the course of processing a single query, so it should not have any side
   * effects (e.g. starting a search in the background).
   *
   * @param p the predicate to match. Must be a tree containing only AND, OR, or NOT predicates as
   *     internal nodes, and {@link IndexPredicate}s as leaves.
   * @param opts query options not implied by the predicate, such as start and limit.
   * @return a source of documents matching the predicate, returned in a defined order depending on
   *     the type of documents.
   * @throws QueryParseException if the predicate could not be converted to an indexed data source.
   */
  DataSource<V> getSource(Predicate<V> p, QueryOptions opts) throws QueryParseException;

  /**
   * Get a single document from the index.
   *
   * @param key document key.
   * @param opts query options. Options that do not make sense in the context of a single document,
   *     such as start, will be ignored.
   * @return a single document if present.
   */
  default Optional<V> get(K key, QueryOptions opts) {
    opts = opts.withStart(0).withLimit(2);
    ImmutableList<V> results;
    try {
      results = getSource(keyPredicate(key), opts).read().toList();
    } catch (QueryParseException e) {
      throw new StorageException("Unexpected QueryParseException during get()", e);
    }
    if (results.size() > 1) {
      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
    }
    return results.stream().findFirst();
  }

  /**
   * Get a single raw document from the index.
   *
   * @param key document key.
   * @param opts query options. Options that do not make sense in the context of a single document,
   *     such as start, will be ignored.
   * @return an abstraction of a raw index document to retrieve fields from.
   */
  default Optional<FieldBundle> getRaw(K key, QueryOptions opts) {
    opts = opts.withStart(0).withLimit(2);
    ImmutableList<FieldBundle> results;
    try {
      results = getSource(keyPredicate(key), opts).readRaw().toList();
    } catch (QueryParseException e) {
      throw new StorageException("Unexpected QueryParseException during get()", e);
    }
    if (results.size() > 1) {
      throw new StorageException("Multiple results found in index for key " + key + ": " + results);
    }
    return results.stream().findFirst();
  }

  /**
   * Get a predicate that looks up a single document by key.
   *
   * @param key document key.
   * @return a single predicate.
   */
  Predicate<V> keyPredicate(K key);

  /**
   * Mark whether this index is up-to-date and ready to serve reads.
   *
   * @param ready whether the index is ready
   */
  void markReady(boolean ready);

  /**
   * Returns whether the index is enabled. {@code true} by default, but could be overridden by
   * implementations.
   */
  default boolean isEnabled() {
    return true;
  }
}
