/*
 * 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.jackrabbit.oak.plugins.document;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.jackrabbit.oak.cache.AbstractCacheStats;
import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Condition;
import org.apache.jackrabbit.oak.plugins.document.cache.CacheInvalidationStats;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static java.util.stream.Collectors.toList;
import static org.apache.jackrabbit.oak.plugins.document.Document.ID;
import static org.apache.jackrabbit.oak.plugins.document.Throttler.NO_THROTTLING;

/**
 * The interface for the backend storage for documents.
 * <p>
 * In general atomicity of operations on a DocumentStore are limited to a single
 * document. That is, an implementation does not have to guarantee atomicity of
 * the entire effect of a method call. A method that fails with an exception may
 * have modified just some documents and then abort. However, an implementation
 * must not modify a document partially. Either the complete update operation is
 * applied to a document or no modification is done at all.
 * <p>
 * The key is the id of a document. Keys are opaque strings. All characters are
 * allowed. Leading and trailing whitespace is allowed. For keys, the maximum
 * length is 512 bytes in the UTF-8 representation.
 */
public interface DocumentStore {

    /**
     * Get the document with the given {@code key}. This is a convenience method
     * and equivalent to {@link #find(Collection, String, int)} with a
     * {@code maxCacheAge} of {@code Integer.MAX_VALUE}.
     * <p>
     * The returned document is immutable.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param key the key
     * @return the document, or null if not found
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @Nullable
    <T extends Document> T find(Collection<T> collection, String key)
            throws DocumentStoreException;

    /**
     * Get the document with the {@code key}. The implementation may serve the
     * document from a cache, but the cached document must not be older than
     * the given {@code maxCacheAge} in milliseconds. An implementation must
     * invalidate a cached document when it detects it is outdated. That is, a
     * subsequent call to {@link #find(Collection, String)} must return the
     * newer version of the document.
     * <p>
     * The returned document is immutable.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param key the key
     * @param maxCacheAge the maximum age of the cached document (in ms)
     * @return the document, or null if not found
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @Nullable
    <T extends Document> T find(Collection<T> collection, String key, int maxCacheAge)
            throws DocumentStoreException;

    /**
     * Get a list of documents where the key is greater than a start value and
     * less than an end value.
     * <p>
     * The returned documents are sorted by key and are immutable.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param fromKey the start value (excluding)
     * @param toKey the end value (excluding)
     * @param limit the maximum number of entries to return (starting with the lowest key)
     * @return the list (possibly empty)
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @NotNull
    <T extends Document> List<T> query(Collection<T> collection,
                                       String fromKey,
                                       String toKey,
                                       int limit) throws DocumentStoreException;

    /**
     * Get a list of documents where the key is greater than a start value and
     * less than an end value <em>and</em> the given "indexed property" is greater
     * or equals the specified value.
     * <p>
     * The indexed property can either be a {@link Long} value, in which case numeric
     * comparison applies, or a {@link Boolean} value, in which case "false" is mapped
     * to "0" and "true" is mapped to "1".
     * <p>
     * The returned documents are sorted by key and are immutable.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param fromKey the start value (excluding)
     * @param toKey the end value (excluding)
     * @param indexedProperty the name of the indexed property (optional)
     * @param startValue the minimum value of the indexed property
     * @param limit the maximum number of entries to return
     * @return the list (possibly empty)
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @NotNull
    <T extends Document> List<T> query(Collection<T> collection,
                                       String fromKey,
                                       String toKey,
                                       String indexedProperty,
                                       long startValue,
                                       int limit) throws DocumentStoreException;

    /**
     * Remove a document. This method does nothing if there is no document
     * with the given key.
     * <p>
     * In case of a {@code DocumentStoreException}, the document with the given
     * key may or may not have been removed from the store. It is the
     * responsibility of the caller to check whether it still exists. The
     * implementation however ensures that the result of the operation is
     * properly reflected in the document cache. That is, an implementation
     * could simply evict the document with the given key.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param key the key
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    <T extends Document> void remove(Collection<T> collection, String key)
            throws DocumentStoreException;

    /**
     * Batch remove documents with given keys. Keys for documents that do not
     * exist are simply ignored. If this method fails with an exception, then
     * only some of the documents identified by {@code keys} may have been
     * removed.
     * <p>
     * In case of a {@code DocumentStoreException}, the documents with the given
     * keys may or may not have been removed from the store. It may also be
     * possible that only some have been removed from the store. It is the
     * responsibility of the caller to check which documents still exist. The
     * implementation however ensures that the result of the operation is
     * properly reflected in the document cache. That is, an implementation
     * could simply evict documents with the given keys from the cache.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param keys list of keys
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    <T extends Document> void remove(Collection<T> collection, List<String> keys)
            throws DocumentStoreException;

    /**
     * Batch remove documents with given keys and corresponding equal conditions
     * on {@link NodeDocument#MODIFIED_IN_SECS} values. Keys for documents that
     * do not exist are simply ignored. A document is only removed if the
     * corresponding condition is met.
     * <p>
     * In case of a {@code DocumentStoreException}, the documents with the given
     * keys may or may not have been removed from the store. It may also be
     * possible that only some have been removed from the store. It is the
     * responsibility of the caller to check which documents still exist. The
     * implementation however ensures that the result of the operation is
     * properly reflected in the document cache. That is, an implementation
     * could simply evict documents with the given keys from the cache.
     *
     * @param <T>
     *            the document type
     * @param collection
     *            the collection.
     * @param toRemove
     *            the keys of the documents to remove with the corresponding
     *            timestamps.
     * @return the number of removed documents.
     * @throws DocumentStoreException
     *             if the operation failed. E.g. because of an I/O error.
     */
    <T extends Document> int remove(Collection<T> collection, Map<String, Long> toRemove)
            throws DocumentStoreException;


    /**
     * Batch remove documents where the given "indexed property" is within the given
     * range (exclusive) - {@code (startValue, endValue)}.
     * <p>
     * The indexed property is a {@link Long} value and numeric comparison applies.
     * <p>
     * In case of a {@code DocumentStoreException}, the documents with the given
     * keys may or may not have been removed from the store. It may also be
     * possible that only some have been removed from the store. It is the
     * responsibility of the caller to check which documents still exist. The
     * implementation however ensures that the result of the operation is
     * properly reflected in the document cache. That is, an implementation
     * could simply evict documents with the given keys from the cache.
     *
     * @param <T> the document type
     * @param collection the collection.
     * @param indexedProperty the name of the indexed property
     * @param startValue the minimum value of the indexed property (exclusive)
     * @param endValue the maximum value of the indexed property (exclusive)
     * @return the number of removed documents.
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    <T extends Document> int remove(Collection<T> collection,
                                    String indexedProperty, long startValue, long endValue)
            throws DocumentStoreException;

    /**
     * Try to create a list of documents. This method returns {@code true} iff
     * none of the documents existed before and the create was successful. This
     * method will return {@code false} if one of the documents already exists
     * in the store. Some documents may still have been created in the store.
     * An implementation does not have to guarantee an atomic create of all the
     * documents described in the {@code updateOps}. It is the responsibility of
     * the caller to check, which documents were created and take appropriate
     * action. The same is true when this method throws
     * {@code DocumentStoreException} (e.g. when a communication error occurs).
     * In this case only some documents may have been created.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param updateOps the list of documents to add (where {@link Condition}s are not allowed)
     * @return true if this worked (if none of the documents already existed)
     * @throws IllegalArgumentException when at least one of the {@linkplain UpdateOp}s is conditional
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    <T extends Document> boolean create(Collection<T> collection,
                                        List<UpdateOp> updateOps)
            throws IllegalArgumentException, DocumentStoreException;

    /**
     * Atomically checks if the document exists and updates it, otherwise the
     * document is created (aka "upsert"), unless the update operation requires
     * the document to be present (see {@link UpdateOp#isNew()}). The returned
     * document is immutable.
     * <p>
     * If this method fails with a {@code DocumentStoreException}, then the
     * document may or may not have been created or updated. It is the
     * responsibility of the caller to check the result e.g. by calling
     * {@link #find(Collection, String)}. The implementation however ensures
     * that the result of the operation is properly reflected in the document
     * cache. That is, an implementation could simply evict documents with the
     * given keys from the cache.
     *
     * @param <T>
     *            the document type
     * @param collection
     *            the collection
     * @param update
     *            the update operation (where {@link Condition}s are not
     *            allowed)
     * @return the old document or {@code null} if it either didn't exist
     *            before, or the {@linkplain UpdateOp} required the document to be
     *            present but {@link UpdateOp#isNew()} was {@code false}.
     * @throws IllegalArgumentException
     *             when the {@linkplain UpdateOp} is conditional
     * @throws DocumentStoreException
     *             if the operation failed. E.g. because of an I/O error.
     */
    @Nullable
    <T extends Document> T createOrUpdate(Collection<T> collection,
                                          UpdateOp update)
            throws IllegalArgumentException, DocumentStoreException;

    /**
     * Create or unconditionally update a number of documents. An implementation
     * does not have to guarantee that all changes are applied atomically,
     * together.
     * <p>
     * In case of a {@code DocumentStoreException} (e.g. when a communication
     * error occurs) only some changes may have been applied. In this case it is
     * the responsibility of the caller to check which {@linkplain UpdateOp}s
     * were applied and take appropriate action. The implementation however
     * ensures that the result of the operations are properly reflected in the
     * document cache. That is, an implementation could simply evict documents
     * related to the given update operations from the cache.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param updateOps the update operation list
     * @return the list containing old documents or <code>null</code> values if they didn't exist
     *         before (see {@linkplain #createOrUpdate(Collection, UpdateOp)}), where the order
     *         reflects the order in the "updateOps" parameter
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    <T extends Document> List<T> createOrUpdate(Collection<T> collection,
                                                List<UpdateOp> updateOps)
            throws DocumentStoreException;

    /**
     * Performs a conditional update (e.g. using
     * {@link UpdateOp.Condition.Type#EXISTS} and only updates the
     * document if the condition is <code>true</code>. The returned document is
     * immutable.
     * <p>
     * In case of a {@code DocumentStoreException} (e.g. when a communication
     * error occurs) the update may or may not have been applied. In this case
     * it is the responsibility of the caller to check whether the update was
     * applied and take appropriate action. The implementation however ensures
     * that the result of the operation is properly reflected in the document
     * cache. That is, an implementation could simply evict the document related
     * to the given update operation from the cache.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param update the update operation with the condition
     * @return the old document or <code>null</code> if the condition is not met or
     *         if the document wasn't found
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     * @see #createOrUpdate(Collection, List)
     */
    @Nullable
    <T extends Document> T findAndUpdate(Collection<T> collection,
                                         UpdateOp update)
            throws DocumentStoreException;

    /**
     * Invalidate the document cache. Calling this method instructs the
     * implementation to invalidate each document from the cache, which is not
     * up to date with the underlying storage at the time this method is called.
     * A document is considered in the cache if {@link #getIfCached(Collection, String)}
     * returns a non-null value for a key.
     * <p>
     * An implementation is allowed to perform lazy invalidation and only check
     * whether a document is up-to-date when it is accessed after this method
     * is called. However, this also includes a call to {@link #getIfCached(Collection, String)},
     * which must only return the document if it was up-to-date at the time
     * this method was called. Similarly, a call to {@link #find(Collection, String)}
     * must guarantee the returned document reflects all the changes done up to
     * when {@code invalidateCache()} was called.
     * <p>
     * In some implementations this method can be a NOP because documents can
     * only be modified through a single instance of a {@code DocumentStore}.
     *
     * @return cache invalidation statistics or {@code null} if none are
     *          available.
     */
    @Nullable
    CacheInvalidationStats invalidateCache();

    /**
     * Invalidate the document cache but only with entries that match one
     * of the keys provided.
     *
     * See {@link #invalidateCache()} for the general contract of cache
     * invalidation.
     *
     * @param keys the keys of the documents to invalidate.
     * @return cache invalidation statistics or {@code null} if none are
     *          available.
     */
    @Nullable
    CacheInvalidationStats invalidateCache(Iterable<String> keys);

    /**
     * Invalidate the document cache for the given key.
     *
     * See {@link #invalidateCache()} for the general contract of cache
     * invalidation.
     *
     * @param collection the collection
     * @param key the key
     */
    <T extends Document> void invalidateCache(Collection<T> collection, String key);

    /**
     * Dispose this instance.
     */
    void dispose();

    /**
     * Fetches the cached document. If the document is not present in the cache
     * {@code null} will be returned. This method is consistent with other find
     * methods that may return cached documents and will return {@code null}
     * even when the implementation has a negative cache for documents that
     * do not exist. This method will never return {@link NodeDocument#NULL}.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param key the key
     * @return cached document if present. Otherwise {@code null}.
     */
    @Nullable
    <T extends Document> T getIfCached(Collection<T> collection, String key);

    /**
     * Set the level of guarantee for read and write operations, if supported by this backend.
     *
     * @param readWriteMode the read/write mode
     */
    void setReadWriteMode(String readWriteMode);

    /**
     * @return status information about the cache
     */
    @Nullable
    Iterable<AbstractCacheStats> getCacheStats();

    /**
     * @return description of the underlying storage.
     */
    Map<String, String> getMetadata();

    /**
     * Returns statistics about the underlying storage. The information and
     * keys returned by this method are implementation specific, may change
     * between releases or may even depend on deployment aspects. E.g. depending
     * on access rights, the method may return more or less information from
     * the underlying store. This method should only be used for informational
     * or debug purposes.
     *
     * @return statistics about this document store.
     */
    @NotNull
    Map<String, String> getStats();

    /**
     * @return the estimated time difference in milliseconds between the local
     * instance and the (typically common, shared) document server system. The
     * value can be zero if the times are estimated to be equal, positive when
     * the local instance is ahead of the remote server and negative when the
     * local instance is behind the remote server. An invocation is not cached
     * and typically requires a round-trip to the server (but that is not a
     * requirement).
     * @throws UnsupportedOperationException if this DocumentStore does not
     *                                       support this method
     * @throws DocumentStoreException if an I/O error occurs.
     */
    long determineServerTimeDifferenceMillis()
            throws UnsupportedOperationException, DocumentStoreException;

    /**
     * optional method indicating the DocumentStore should prefetch, in the most
     * optimal way eg by batching, a bunch of keys as they might soon be needed.
     */
    default <T extends Document> void prefetch(Collection<T> collection, Iterable<String> keys) {
        // default does nothing
    }

    /**
     * Return the size limit for node name based on the document store implementation
     *
     * @return node name size limit
     */
    default int getNodeNameLimit() {
        return Utils.NODE_NAME_LIMIT;
    }

    /**
     * Return the {@link Throttler} for the underlying store
     * Default is no throttling
     *
     * @return throttler for document store
     */
    default Throttler throttler() {
        return NO_THROTTLING;
    }

    /**
     * Get a list of documents with only projected fields (as mentioned in projections param)
     * along with "_id" field and where the key is greater than a start value and
     * less than an end value <em>and</em> the given "indexed property" is greater
     * or equals the specified value.
     * <p>
     * The indexed property can either be a {@link Long} value, in which case numeric
     * comparison applies, or a {@link Boolean} value, in which case "false" is mapped
     * to "0" and "true" is mapped to "1".
     * <p>
     * The returned documents are sorted by key and are immutable.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param fromKey the start value (excluding)
     * @param toKey the end value (excluding)
     * @param indexedProperty the name of the indexed property (optional)
     * @param startValue the minimum value of the indexed property
     * @param limit the maximum number of entries to return
     * @param projection {@link List} of projected keys (optional). Keep this empty to fetch all fields on document.
     * @return the list (possibly empty)
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @NotNull
    default <T extends Document> List<T> query(final Collection<T> collection,
                                               final String fromKey,
                                               final String toKey,
                                               final String indexedProperty,
                                               final long startValue,
                                               final int limit,
                                               final List<String> projection) throws DocumentStoreException {

        final List<T> list = query(collection, fromKey, toKey, indexedProperty, startValue, limit);

        if (projection == null || projection.isEmpty()) {
            return list;
        }

        final Set<String> projectedSet = new HashSet<>(projection);
        projectedSet.add(ID);

        return list.stream().map(t -> {
            T newDocument = collection.newDocument(this);
            t.deepCopy(newDocument);
            newDocument.keySet().retainAll(projectedSet);
            return newDocument;
        }).collect(Collectors.toList());
    }

    /**
     * Performs a conditional update (e.g. using
     * {@link UpdateOp.Condition.Type#EXISTS} and only update the
     * document if the condition is <code>true</code>. The returned documents are
     * immutable.
     * <p>
     * In case of a {@code DocumentStoreException} (e.g. when a communication
     * error occurs) only some updates may have been applied. In this case
     * it is the responsibility of the caller to check which {@link UpdateOp}s
     * were applied and take appropriate action. The implementation however ensures
     * that the result of the operations are properly reflected in the document
     * cache. That is, an implementation could simply evict the documents related
     * to the given update operations from the cache.
     *
     * @param <T> the document type
     * @param collection the collection
     * @param updateOps the update operation List
     * @return the list containing old documents or <code>null</code> if the condition is not met
     *         or if the document wasn't found. The order in the result list reflects the order
     *         in the updateOps parameter
     * @throws DocumentStoreException if the operation failed. E.g. because of
     *          an I/O error.
     */
    @NotNull
    default <T extends Document> List<T> findAndUpdate(final @NotNull Collection<T> collection,
                                                       final @NotNull List<UpdateOp> updateOps) throws DocumentStoreException {
        return updateOps.stream().map(op -> findAndUpdate(collection, op)).collect(toList());
    }
}
