// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.remote.common;

import static com.google.common.util.concurrent.MoreExecutors.directExecutor;

import build.bazel.remote.execution.v2.Action;
import build.bazel.remote.execution.v2.ActionResult;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.ServerCapabilities;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.devtools.build.lib.remote.util.AsyncTaskCache;
import com.google.devtools.build.lib.remote.util.RxFutures;
import com.google.devtools.build.lib.vfs.Path;
import com.google.protobuf.ByteString;
import io.reactivex.rxjava3.functions.Supplier;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Base class for a remote caching protocol.
 *
 * <p>Concurrent uploads of the same digest are deduplicated: only one network upload is performed
 * per digest at a time, and subsequent callers attach as observers to the in-flight upload.
 * Implementations provide the raw network calls via the {@code *Impl} methods.
 *
 * <p>Implementations must be thread-safe.
 */
public abstract class RemoteCacheClient implements MissingDigestsFinder {

  private final AsyncTaskCache.NoResult<Digest> casUploadCache = AsyncTaskCache.NoResult.create();

  public abstract ServerCapabilities getServerCapabilities() throws IOException;

  public abstract ListenableFuture<String> getAuthority();

  /**
   * Downloads an action result for the {@code actionKey}.
   *
   * @param context the context for the action.
   * @param actionKey The digest of the {@link Action} that generated the action result.
   * @param inlineOutErr A hint to the server to inline the stdout and stderr in the {@code
   *     ActionResult} message.
   * @param inlineOutputFiles A hint to the server to inline the specified output files in the
   *     {@code ActionResult} message.
   * @return A Future representing pending download of an action result. If an action result for
   *     {@code actionKey} cannot be found the result of the Future is {@code null}.
   */
  public abstract ListenableFuture<ActionResult> downloadActionResult(
      RemoteActionExecutionContext context,
      ActionKey actionKey,
      boolean inlineOutErr,
      Set<String> inlineOutputFiles);

  /**
   * Uploads an action result for the {@code actionKey}.
   *
   * @param context the context for the action.
   * @param actionKey The digest of the {@link Action} that generated the action result.
   * @param actionResult The action result to associate with the {@code actionKey}.
   * @return A Future representing pending completion of the upload.
   */
  public abstract ListenableFuture<Void> uploadActionResult(
      RemoteActionExecutionContext context, ActionKey actionKey, ActionResult actionResult);

  /**
   * Downloads a BLOB for the given {@code digest} and writes it to {@code out}.
   *
   * <p>It's the callers responsibility to close {@code out}.
   *
   * @param context the context for the action.
   * @return A Future representing pending completion of the download. If a BLOB for {@code digest}
   *     does not exist in the cache the Future fails with a {@link CacheNotFoundException}.
   */
  public abstract ListenableFuture<Void> downloadBlob(
      RemoteActionExecutionContext context, Digest digest, OutputStream out);

  /**
   * A supplier for the data comprising a BLOB.
   *
   * <p>As blobs can be large and may need to be kept in memory, consumers should call {@link #get}
   * as late as possible.
   */
  @FunctionalInterface
  public interface Blob {
    /** Get an input stream for the blob's data. Can be called multiple times. */
    InputStream get() throws IOException;

    /** An optional human-readable description of the blob's source. */
    @Nullable
    default String description() {
      return null;
    }
  }

  /**
   * Uploads a {@code file} BLOB to the CAS.
   *
   * <p>Concurrent uploads of the same digest are deduplicated. If {@code force} is true an upload
   * that has already finished is re-executed.
   *
   * @param context the context for the action.
   * @param digest The digest of the file.
   * @param file The file to upload.
   */
  public final ListenableFuture<Void> uploadFile(
      RemoteActionExecutionContext context, Digest digest, Path file, boolean force) {
    return uploadBlob(
        context,
        digest,
        new Blob() {
          @Override
          public InputStream get() {
            return new LazyFileInputStream(file);
          }

          @Override
          public String description() {
            return "file " + file;
          }
        },
        force);
  }

  /**
   * Uploads a blob to the CAS.
   *
   * <p>Concurrent uploads of the same digest are deduplicated. If {@code force} is true an upload
   * that has already finished is re-executed.
   *
   * @param context the context for the action.
   * @param digest The digest of the blob.
   * @param blob A supplier for the blob to upload. May be called multiple times, but is closed by
   *     the implementation after the upload is complete.
   */
  public final ListenableFuture<Void> uploadBlob(
      RemoteActionExecutionContext context, Digest digest, Blob blob, boolean force) {
    return RxFutures.toListenableFuture(
        casUploadCache.execute(
            digest,
            RxFutures.toCompletable(() -> uploadBlobImpl(context, digest, blob), directExecutor()),
            force));
  }

  /**
   * Uploads an in-memory BLOB to the CAS.
   *
   * <p>Concurrent uploads of the same digest are deduplicated. If {@code force} is true an upload
   * that has already finished is re-executed.
   *
   * @param context the context for the action.
   * @param digest The digest of the blob.
   * @param data The BLOB to upload.
   */
  public final ListenableFuture<Void> uploadBlob(
      RemoteActionExecutionContext context, Digest digest, ByteString data, boolean force) {
    return uploadBlob(context, digest, (Blob) data::newInput, force);
  }

  /**
   * Performs the actual network upload. Called by the deduplicating {@link #uploadBlob} wrappers.
   *
   * <p>Callers should use {@link #uploadBlob} instead.
   */
  @VisibleForTesting
  public abstract ListenableFuture<Void> uploadBlobImpl(
      RemoteActionExecutionContext context, Digest digest, Blob blob);

  /**
   * Registers a blob as the concatenation of the given chunks via SpliceBlob RPC.
   *
   * <p>This is used for CDC (Content-Defined Chunking) uploads. After uploading all chunks,
   * SpliceBlob is called to register the blob with the given digest as the concatenation of the
   * chunks.
   *
   * @param context the context for the action.
   * @param blobDigest The digest of the complete blob.
   * @param chunkDigests The digests of the chunks that make up the blob, in order.
   * @return A future representing pending completion of the splice operation, or null if SpliceBlob
   *     is not supported by this cache client.
   */
  @Nullable
  public ListenableFuture<Void> spliceBlob(
      RemoteActionExecutionContext context, Digest blobDigest, List<Digest> chunkDigests) {
    return null;
  }

  /**
   * Deduplicates an upload by digest using the same cache as {@link #uploadFile} and {@link
   * #uploadBlob}. For use by callers that perform their own upload logic but want to share the
   * dedup state with the regular upload paths (e.g. chunked uploads).
   */
  public final ListenableFuture<Void> dedupUpload(
      Digest digest, Supplier<ListenableFuture<Void>> upload, boolean force) {
    return RxFutures.toListenableFuture(
        casUploadCache.execute(digest, RxFutures.toCompletable(upload, directExecutor()), force));
  }

  /** Returns the digests currently being uploaded. */
  public final ImmutableSet<Digest> getInProgressUploads() {
    return casUploadCache.getInProgressTasks();
  }

  /** Returns the digests for which an upload has finished successfully. */
  public final ImmutableSet<Digest> getFinishedUploads() {
    return casUploadCache.getFinishedTasks();
  }

  /** Returns the number of subscribers waiting for an in-progress upload of {@code digest}. */
  public final int getUploadSubscriberCount(Digest digest) {
    return casUploadCache.getSubscriberCount(digest);
  }

  /** Stops accepting new uploads. */
  public final void shutdownUploads() {
    casUploadCache.shutdown();
  }

  /** Waits for in-progress uploads to finish. */
  public final void awaitUploadTermination() throws InterruptedException {
    casUploadCache.awaitTermination();
  }

  /** Cancels in-progress uploads. */
  public final void shutdownUploadsNow() {
    casUploadCache.shutdownNow();
  }

  /** Close resources associated with the remote cache. */
  public abstract void close();
}
