// 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.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputFileCache;
import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
import com.google.devtools.build.lib.remote.RemoteProtocol.BlobChunk;
import com.google.devtools.build.lib.remote.RemoteProtocol.ContentDigest;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.Path;
import com.google.protobuf.ByteString;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;

/** An iterator-type object that transforms byte sources into a stream of BlobChunk messages. */
public final class Chunker {
  /** An Item is an opaque digestable source of bytes. */
  interface Item {
    ContentDigest getDigest() throws IOException;

    InputStream getInputStream() throws IOException;
  }

  private final Iterator<Item> inputIterator;
  private InputStream currentStream;
  private final Set<ContentDigest> digests;
  private ContentDigest digest;
  private long bytesLeft;
  private final int chunkSize;

  Chunker(
      Iterator<Item> inputIterator,
      int chunkSize,
      // If present, specifies which digests to output out of the whole input.
      @Nullable Set<ContentDigest> digests)
      throws IOException {
    Preconditions.checkArgument(chunkSize > 0, "Chunk size must be greater than 0");
    this.digests = digests;
    this.inputIterator = inputIterator;
    this.chunkSize = chunkSize;
    advanceInput();
  }

  Chunker(Iterator<Item> inputIterator, int chunkSize) throws IOException {
    this(inputIterator, chunkSize, null);
  }

  Chunker(Item input, int chunkSize) throws IOException {
    this(Iterators.singletonIterator(input), chunkSize, ImmutableSet.of(input.getDigest()));
  }

  private void advanceInput() throws IOException {
    do {
      if (inputIterator != null && inputIterator.hasNext()) {
        Item input = inputIterator.next();
        digest = input.getDigest();
        currentStream = input.getInputStream();
        bytesLeft = digest.getSizeBytes();
      } else {
        digest = null;
        currentStream = null;
        bytesLeft = 0;
      }
    } while (digest != null && digests != null && !digests.contains(digest));
  }

  /** True if the object has more BlobChunk elements. */
  public boolean hasNext() {
    return currentStream != null;
  }

  /** Consume the next BlobChunk element. */
  public BlobChunk next() throws IOException {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }
    BlobChunk.Builder chunk = BlobChunk.newBuilder();
    long offset = digest.getSizeBytes() - bytesLeft;
    if (offset == 0) {
      chunk.setDigest(digest);
    } else {
      chunk.setOffset(offset);
    }
    if (bytesLeft > 0) {
      byte[] blob = new byte[(int) Math.min(bytesLeft, chunkSize)];
      currentStream.read(blob);
      chunk.setData(ByteString.copyFrom(blob));
      bytesLeft -= blob.length;
    }
    if (bytesLeft == 0) {
      currentStream.close();
      advanceInput(); // Sets the current stream to null, if it was the last.
    }
    return chunk.build();
  }

  static Item toItem(final byte[] blob) {
    return new Item() {
      @Override
      public ContentDigest getDigest() throws IOException {
        return ContentDigests.computeDigest(blob);
      }

      @Override
      public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(blob);
      }
    };
  }

  static Item toItem(final Path file) {
    return new Item() {
      @Override
      public ContentDigest getDigest() throws IOException {
        return ContentDigests.computeDigest(file);
      }

      @Override
      public InputStream getInputStream() throws IOException {
        return file.getInputStream();
      }
    };
  }

  static Item toItem(
      final ActionInput input, final ActionInputFileCache inputCache, final Path execRoot) {
    if (input instanceof VirtualActionInput) {
      return toItem((VirtualActionInput) input);
    }
    return new Item() {
      @Override
      public ContentDigest getDigest() throws IOException {
        return ContentDigests.getDigestFromInputCache(input, inputCache);
      }

      @Override
      public InputStream getInputStream() throws IOException {
        return execRoot.getRelative(input.getExecPathString()).getInputStream();
      }
    };
  }

  static Item toItem(final VirtualActionInput input) {
    return new Item() {
      @Override
      public ContentDigest getDigest() throws IOException {
        return ContentDigests.computeDigest(input);
      }

      @Override
      public InputStream getInputStream() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        input.writeTo(buffer);
        return new ByteArrayInputStream(buffer.toByteArray());
      }
    };
  }

  /**
   * Create a Chunker from a given ActionInput, taking its digest from the provided
   * ActionInputFileCache.
   */
  public static Chunker from(
      ActionInput input, int chunkSize, ActionInputFileCache inputCache, Path execRoot)
      throws IOException {
    return new Chunker(toItem(input, inputCache, execRoot), chunkSize);
  }

  /** Create a Chunker from a given blob and chunkSize. */
  public static Chunker from(byte[] blob, int chunkSize) throws IOException {
    return new Chunker(toItem(blob), chunkSize);
  }

  /** Create a Chunker from a given Path and chunkSize. */
  public static Chunker from(Path file, int chunkSize) throws IOException {
    return new Chunker(toItem(file), chunkSize);
  }

  /**
   * Create a Chunker from multiple input sources. The order of the sources provided to the Builder
   * will be the same order they will be chunked by.
   */
  public static final class Builder {
    private final ArrayList<Item> items = new ArrayList<>();
    private Set<ContentDigest> digests = null;
    private int chunkSize = 0;

    public Chunker build() throws IOException {
      return new Chunker(items.iterator(), chunkSize, digests);
    }

    public Builder chunkSize(int chunkSize) {
      this.chunkSize = chunkSize;
      return this;
    }

    /**
     * Restricts the Chunker to use only inputs with these digests. This is an optimization for CAS
     * uploads where a list of digests missing from the CAS is known.
     */
    public Builder onlyUseDigests(Set<ContentDigest> digests) {
      this.digests = digests;
      return this;
    }

    public Builder addInput(byte[] blob) {
      items.add(toItem(blob));
      return this;
    }

    public Builder addInput(Path file) {
      items.add(toItem(file));
      return this;
    }

    public Builder addInput(ActionInput input, ActionInputFileCache inputCache, Path execRoot) {
      items.add(toItem(input, inputCache, execRoot));
      return this;
    }

    public Builder addAllInputs(
        Collection<? extends ActionInput> inputs, ActionInputFileCache inputCache, Path execRoot) {
      for (ActionInput input : inputs) {
        items.add(toItem(input, inputCache, execRoot));
      }
      return this;
    }
  }
}
