// Copyright 2016 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;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.remote.ContentDigests.ActionKey;
import com.google.devtools.build.lib.remote.RemoteProtocol.ActionResult;
import com.google.devtools.build.lib.remote.RemoteProtocol.ContentDigest;
import com.google.devtools.build.lib.remote.RemoteProtocol.FileMetadata;
import com.google.devtools.build.lib.remote.RemoteProtocol.FileNode;
import com.google.devtools.build.lib.remote.RemoteProtocol.Output;
import com.google.devtools.build.lib.remote.RemoteProtocol.Output.ContentCase;
import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;

/**
 * A RemoteActionCache implementation that uses a concurrent map as a distributed storage for files
 * and action output.
 *
 * <p>The thread safety is guaranteed by the underlying map.
 */
@ThreadSafe
public final class ConcurrentMapActionCache implements RemoteActionCache {
  private final ConcurrentMap<String, byte[]> cache;
  private static final int MAX_MEMORY_KBYTES = 512 * 1024;
  private final Semaphore uploadMemoryAvailable = new Semaphore(MAX_MEMORY_KBYTES, true);

  public ConcurrentMapActionCache(ConcurrentMap<String, byte[]> cache) {
    this.cache = cache;
  }

  @Override
  public void uploadTree(TreeNodeRepository repository, Path execRoot, TreeNode root)
      throws IOException, InterruptedException {
    repository.computeMerkleDigests(root);
    for (FileNode fileNode : repository.treeToFileNodes(root)) {
      uploadBlob(fileNode.toByteArray());
    }
    for (TreeNode leaf : repository.leaves(root)) {
      uploadFileContents(execRoot.getRelative(leaf.getActionInput().getExecPathString()));
    }
  }

  @Override
  public void downloadTree(ContentDigest rootDigest, Path rootLocation)
      throws IOException, CacheNotFoundException {
    FileNode fileNode = FileNode.parseFrom(downloadBlob(rootDigest));
    if (fileNode.hasFileMetadata()) {
      FileMetadata meta = fileNode.getFileMetadata();
      downloadFileContents(meta.getDigest(), rootLocation, meta.getExecutable());
    }
    for (FileNode.Child child : fileNode.getChildList()) {
      downloadTree(child.getDigest(), rootLocation.getRelative(child.getPath()));
    }
  }

  @Override
  public ContentDigest uploadFileContents(Path file) throws IOException, InterruptedException {
    // This unconditionally reads the whole file into memory first!
    return uploadBlob(ByteString.readFrom(file.getInputStream()).toByteArray());
  }

  @Override
  public void downloadAllResults(ActionResult result, Path execRoot)
      throws IOException, CacheNotFoundException {
    for (Output output : result.getOutputList()) {
      if (output.getContentCase() == ContentCase.FILE_METADATA) {
        FileMetadata m = output.getFileMetadata();
        downloadFileContents(
            m.getDigest(), execRoot.getRelative(output.getPath()), m.getExecutable());
      } else {
        downloadTree(output.getDigest(), execRoot.getRelative(output.getPath()));
      }
    }
  }

  @Override
  public void uploadAllResults(Path execRoot, Collection<Path> files, ActionResult.Builder result)
      throws IOException, InterruptedException {
    for (Path file : files) {
      if (file.isDirectory()) {
        // TODO(olaola): to implement this for a directory, will need to create or pass a
        // TreeNodeRepository to call uploadTree.
        throw new UnsupportedOperationException("Storing a directory is not yet supported.");
      }
      // First put the file content to cache.
      ContentDigest digest = uploadFileContents(file);
      // Add to protobuf.
      result
          .addOutputBuilder()
          .setPath(file.relativeTo(execRoot).getPathString())
          .getFileMetadataBuilder()
          .setDigest(digest)
          .setExecutable(file.isExecutable());
    }
  }

  @Override
  public void downloadFileContents(ContentDigest digest, Path dest, boolean executable)
      throws IOException, CacheNotFoundException {
    // This unconditionally downloads the whole file into memory first!
    byte[] contents = downloadBlob(digest);
    FileSystemUtils.createDirectoryAndParents(dest.getParentDirectory());
    try (OutputStream stream = dest.getOutputStream()) {
      stream.write(contents);
    }
    dest.setExecutable(executable);
  }

  @Override
  public ImmutableList<ContentDigest> uploadBlobs(Iterable<byte[]> blobs)
      throws InterruptedException {
    ArrayList<ContentDigest> digests = new ArrayList<>();
    for (byte[] blob : blobs) {
      digests.add(uploadBlob(blob));
    }
    return ImmutableList.copyOf(digests);
  }

  @Override
  public ContentDigest uploadBlob(byte[] blob) throws InterruptedException {
    int blobSizeKBytes = blob.length / 1024;
    Preconditions.checkArgument(blobSizeKBytes < MAX_MEMORY_KBYTES);
    ContentDigest digest = ContentDigests.computeDigest(blob);
    uploadMemoryAvailable.acquire(blobSizeKBytes);
    try {
      cache.put(ContentDigests.toHexString(digest), blob);
    } finally {
      uploadMemoryAvailable.release(blobSizeKBytes);
    }
    return digest;
  }

  @Override
  public byte[] downloadBlob(ContentDigest digest) throws CacheNotFoundException {
    if (digest.getSizeBytes() == 0) {
      return new byte[0];
    }
    // This unconditionally downloads the whole blob into memory!
    Preconditions.checkArgument((int) (digest.getSizeBytes() / 1024) < MAX_MEMORY_KBYTES);
    byte[] data = cache.get(ContentDigests.toHexString(digest));
    if (data == null) {
      throw new CacheNotFoundException(digest);
    }
    return data;
  }

  @Override
  public ImmutableList<byte[]> downloadBlobs(Iterable<ContentDigest> digests)
      throws CacheNotFoundException {
    ArrayList<byte[]> blobs = new ArrayList<>();
    for (ContentDigest c : digests) {
      blobs.add(downloadBlob(c));
    }
    return ImmutableList.copyOf(blobs);
  }

  public boolean containsKey(ContentDigest digest) {
    return cache.containsKey(ContentDigests.toHexString(digest));
  }

  @Override
  public ActionResult getCachedActionResult(ActionKey actionKey) {
    byte[] data = cache.get(ContentDigests.toHexString(actionKey.getDigest()));
    if (data == null) {
      return null;
    }
    try {
      return ActionResult.parseFrom(data);
    } catch (InvalidProtocolBufferException e) {
      return null;
    }
  }

  @Override
  public void setCachedActionResult(ActionKey actionKey, ActionResult result)
      throws InterruptedException {
    cache.put(ContentDigests.toHexString(actionKey.getDigest()), result.toByteArray());
  }
}
