/*
 * Copyright 2015 The gRPC Authors
 *
 * 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 io.grpc;

import static com.google.common.base.Preconditions.checkArgument;
import static io.grpc.TimeUtils.convertToNanos;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.CheckReturnValue;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
 * The collection of runtime options for a new RPC call.
 *
 * <p>A field that is not set is {@code null}.
 */
@Immutable
@CheckReturnValue
public final class CallOptions {
  /**
   * A blank {@code CallOptions} that all fields are not set.
   */
  public static final CallOptions DEFAULT;

  static {
    Builder b = new Builder();
    b.customOptions = new Object[0][2];
    b.streamTracerFactories = Collections.emptyList();
    DEFAULT = b.build();
  }

  @Nullable
  private final Deadline deadline;

  @Nullable
  private final Executor executor;

  @Nullable
  private final String authority;

  @Nullable
  private final CallCredentials credentials;

  @Nullable
  private final String compressorName;

  private final Object[][] customOptions;

  private final List<ClientStreamTracer.Factory> streamTracerFactories;

  /**
   * Opposite to fail fast.
   */
  @Nullable
  private final Boolean waitForReady;

  @Nullable
  private final Integer maxInboundMessageSize;
  @Nullable
  private final Integer maxOutboundMessageSize;
  @Nullable
  private final Integer onReadyThreshold;

  private CallOptions(Builder builder) {
    this.deadline = builder.deadline;
    this.executor = builder.executor;
    this.authority = builder.authority;
    this.credentials = builder.credentials;
    this.compressorName = builder.compressorName;
    this.customOptions = builder.customOptions;
    this.streamTracerFactories = builder.streamTracerFactories;
    this.waitForReady = builder.waitForReady;
    this.maxInboundMessageSize = builder.maxInboundMessageSize;
    this.maxOutboundMessageSize = builder.maxOutboundMessageSize;
    this.onReadyThreshold = builder.onReadyThreshold;
  }

  static class Builder {
    Deadline deadline;
    Executor executor;
    String authority;
    CallCredentials credentials;
    String compressorName;
    Object[][] customOptions;
    // Unmodifiable list
    List<ClientStreamTracer.Factory> streamTracerFactories;
    Boolean waitForReady;
    Integer maxInboundMessageSize;
    Integer maxOutboundMessageSize;
    Integer onReadyThreshold;

    private CallOptions build() {
      return new CallOptions(this);
    }
  }

  /**
   * Override the HTTP/2 authority the channel claims to be connecting to. <em>This is not
   * generally safe.</em> Overriding allows advanced users to re-use a single Channel for multiple
   * services, even if those services are hosted on different domain names. That assumes the
   * server is virtually hosting multiple domains and is guaranteed to continue doing so. It is
   * rare for a service provider to make such a guarantee. <em>At this time, there is no security
   * verification of the overridden value, such as making sure the authority matches the server's
   * TLS certificate.</em>
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1767")
  public CallOptions withAuthority(@Nullable String authority) {
    Builder builder = toBuilder(this);
    builder.authority = authority;
    return builder.build();
  }

  /**
   * Returns a new {@code CallOptions} with the given call credentials.
   */
  public CallOptions withCallCredentials(@Nullable CallCredentials credentials) {
    Builder builder = toBuilder(this);
    builder.credentials = credentials;
    return builder.build();
  }

  /**
   * Sets the compression to use for the call.  The compressor must be a valid name known in the
   * {@link CompressorRegistry}.  By default, the "gzip" compressor will be available.
   *
   * <p>It is only safe to call this if the server supports the compression format chosen. There is
   * no negotiation performed; if the server does not support the compression chosen, the call will
   * fail.
   */
  public CallOptions withCompression(@Nullable String compressorName) {
    Builder builder = toBuilder(this);
    builder.compressorName = compressorName;
    return builder.build();
  }

  /**
   * Returns a new {@code CallOptions} with the given absolute deadline.
   *
   * <p>This is mostly used for propagating an existing deadline. {@link #withDeadlineAfter} is the
   * recommended way of setting a new deadline,
   *
   * @param deadline the deadline or {@code null} for unsetting the deadline.
   */
  public CallOptions withDeadline(@Nullable Deadline deadline) {
    Builder builder = toBuilder(this);
    builder.deadline = deadline;
    return builder.build();
  }

  /**
   * Returns a new {@code CallOptions} with a deadline that is after the given {@code duration} from
   * now.
   */
  public CallOptions withDeadlineAfter(long duration, TimeUnit unit) {
    return withDeadline(Deadline.after(duration, unit));
  }

  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657")
  public CallOptions withDeadlineAfter(Duration duration) {
    return withDeadlineAfter(convertToNanos(duration), TimeUnit.NANOSECONDS);
  }

  /**
   * Returns the deadline or {@code null} if the deadline is not set.
   */
  @Nullable
  public Deadline getDeadline() {
    return deadline;
  }

  /**
   * Enables <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">
   * 'wait for ready'</a> for the call. Wait-for-ready queues the RPC until a connection is
   * available. This may dramatically increase the latency of the RPC, but avoids failing
   * "unnecessarily." The default queues the RPC until an attempt to connect has completed, but
   * fails RPCs without sending them if unable to connect.
   */
  public CallOptions withWaitForReady() {
    Builder builder = toBuilder(this);
    builder.waitForReady = Boolean.TRUE;
    return builder.build();
  }

  /**
   * Disables 'wait for ready' feature for the call.
   * This method should be rarely used because the default is without 'wait for ready'.
   */
  public CallOptions withoutWaitForReady() {
    Builder builder = toBuilder(this);
    builder.waitForReady = Boolean.FALSE;
    return builder.build();
  }

  /**
   * Specifies how many bytes must be queued before the call is
   * considered not ready to send more messages.
   *
   * @param numBytes The number of bytes that must be queued. Must be a
   *                 positive integer.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11021")
  public CallOptions withOnReadyThreshold(int numBytes) {
    checkArgument(numBytes > 0, "numBytes must be positive: %s", numBytes);
    Builder builder = toBuilder(this);
    builder.onReadyThreshold = numBytes;
    return builder.build();
  }

  /**
   * Resets to the default number of bytes that must be queued before the
   * call will leave the <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">
   * 'wait for ready'</a> state.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11021")
  public CallOptions clearOnReadyThreshold() {
    Builder builder = toBuilder(this);
    builder.onReadyThreshold = null;
    return builder.build();
  }

  /**
   * Returns to the default number of bytes that must be queued before the
   * call will leave the <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">
   * 'wait for ready'</a> state.
   *
   * @return null if the default threshold is used.
   */
  @Nullable
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11021")
  public Integer getOnReadyThreshold() {
    return onReadyThreshold;
  }

  /**
   * Returns the compressor's name.
   */
  @Nullable
  public String getCompressor() {
    return compressorName;
  }

  /**
   * Override the HTTP/2 authority the channel claims to be connecting to. <em>This is not
   * generally safe.</em> Overriding allows advanced users to re-use a single Channel for multiple
   * services, even if those services are hosted on different domain names. That assumes the
   * server is virtually hosting multiple domains and is guaranteed to continue doing so. It is
   * rare for a service provider to make such a guarantee. <em>At this time, there is no security
   * verification of the overridden value, such as making sure the authority matches the server's
   * TLS certificate.</em>
   */
  @Nullable
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1767")
  public String getAuthority() {
    return authority;
  }

  /**
   * Returns the call credentials.
   */
  @Nullable
  public CallCredentials getCredentials() {
    return credentials;
  }

  /**
   * Returns a new {@code CallOptions} with {@code executor} to be used instead of the default
   * executor specified with {@link ManagedChannelBuilder#executor}.
   */
  public CallOptions withExecutor(@Nullable Executor executor) {
    Builder builder = toBuilder(this);
    builder.executor = executor;
    return builder.build();
  }

  /**
   * Returns a new {@code CallOptions} with a {@code ClientStreamTracerFactory} in addition to
   * the existing factories.
   *
   * <p>This method doesn't replace existing factories, or try to de-duplicate factories.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
  public CallOptions withStreamTracerFactory(ClientStreamTracer.Factory factory) {
    ArrayList<ClientStreamTracer.Factory> newList =
        new ArrayList<>(streamTracerFactories.size() + 1);
    newList.addAll(streamTracerFactories);
    newList.add(factory);
    Builder builder = toBuilder(this);
    builder.streamTracerFactories = Collections.unmodifiableList(newList);
    return builder.build();
  }

  /**
   * Returns an immutable list of {@code ClientStreamTracerFactory}s.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
  public List<ClientStreamTracer.Factory> getStreamTracerFactories() {
    return streamTracerFactories;
  }

  /**
   * Key for a key-value pair. Uses reference equality.
   */
  public static final class Key<T> {
    private final String debugString;
    private final T defaultValue;

    private Key(String debugString, T defaultValue) {
      this.debugString = debugString;
      this.defaultValue = defaultValue;
    }

    /**
     * Returns the user supplied default value for this key.
     */
    public T getDefault() {
      return defaultValue;
    }

    @Override
    public String toString() {
      return debugString;
    }

    /**
     * Factory method for creating instances of {@link Key}.
     *
     * @param debugString a string used to describe this key, used for debugging.
     * @param defaultValue default value to return when value for key not set
     * @param <T> Key type
     * @return Key object
     * @deprecated Use {@link #create} or {@link #createWithDefault} instead. This method will
     *     be removed.
     */
    @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1869")
    @Deprecated
    public static <T> Key<T> of(String debugString, T defaultValue) {
      Preconditions.checkNotNull(debugString, "debugString");
      return new Key<>(debugString, defaultValue);
    }

    /**
     * Factory method for creating instances of {@link Key}. The default value of the
     * key is {@code null}.
     *
     * @param debugString a debug string that describes this key.
     * @param <T> Key type
     * @return Key object
     * @since 1.13.0
     */
    public static <T> Key<T> create(String debugString) {
      Preconditions.checkNotNull(debugString, "debugString");
      return new Key<>(debugString, /*defaultValue=*/ null);
    }

    /**
     * Factory method for creating instances of {@link Key}.
     *
     * @param debugString a debug string that describes this key.
     * @param defaultValue default value to return when value for key not set
     * @param <T> Key type
     * @return Key object
     * @since 1.13.0
     */
    public static <T> Key<T> createWithDefault(String debugString, T defaultValue) {
      Preconditions.checkNotNull(debugString, "debugString");
      return new Key<>(debugString, defaultValue);
    }
  }

  /**
   * Sets a custom option. Any existing value for the key is overwritten.
   *
   * @param key The option key
   * @param value The option value.
   * @since 1.13.0
   */
  public <T> CallOptions withOption(Key<T> key, T value) {
    Preconditions.checkNotNull(key, "key");
    Preconditions.checkNotNull(value, "value");

    Builder builder = toBuilder(this);
    int existingIdx = -1;
    for (int i = 0; i < customOptions.length; i++) {
      if (key.equals(customOptions[i][0])) {
        existingIdx = i;
        break;
      }
    }

    builder.customOptions = new Object[customOptions.length + (existingIdx == -1 ? 1 : 0)][2];
    System.arraycopy(customOptions, 0, builder.customOptions, 0, customOptions.length);

    if (existingIdx == -1) {
      // Add a new option
      builder.customOptions[customOptions.length] = new Object[] {key, value};
    } else {
      // Replace an existing option
      builder.customOptions[existingIdx] = new Object[] {key, value};
    }

    return builder.build();
  }

  /**
   * Get the value for a custom option or its inherent default.
   * @param key Key identifying option
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1869")
  @SuppressWarnings("unchecked")
  public <T> T getOption(Key<T> key) {
    Preconditions.checkNotNull(key, "key");
    for (int i = 0; i < customOptions.length; i++) {
      if (key.equals(customOptions[i][0])) {
        return (T) customOptions[i][1];
      }
    }
    return key.defaultValue;
  }

  /**
   * Returns the executor override to use for this specific call, or {@code null} if there is no
   * override. The executor is only for servicing this one call, so is not safe to use after
   * {@link ClientCall.Listener#onClose}.
   */
  @Nullable
  public Executor getExecutor() {
    return executor;
  }

  /**
   * Returns whether <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">
   * 'wait for ready'</a> option is enabled for the call. 'Fail fast' is the default option for gRPC
   * calls and 'wait for ready' is the opposite to it.
   */
  public boolean isWaitForReady() {
    return Boolean.TRUE.equals(waitForReady);
  }

  Boolean getWaitForReady() {
    return waitForReady;
  }

  /**
   * Sets the maximum allowed message size acceptable from the remote peer.  If unset, this will
   * default to the value set on the {@link ManagedChannelBuilder#maxInboundMessageSize(int)}.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
  public CallOptions withMaxInboundMessageSize(int maxSize) {
    checkArgument(maxSize >= 0, "invalid maxsize %s", maxSize);
    Builder builder = toBuilder(this);
    builder.maxInboundMessageSize = maxSize;
    return builder.build();
  }

  /**
   * Sets the maximum allowed message size acceptable sent to the remote peer.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
  public CallOptions withMaxOutboundMessageSize(int maxSize) {
    checkArgument(maxSize >= 0, "invalid maxsize %s", maxSize);
    Builder builder = toBuilder(this);
    builder.maxOutboundMessageSize = maxSize;
    return builder.build();
  }

  /**
   * Gets the maximum allowed message size acceptable from the remote peer.
   */
  @Nullable
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
  public Integer getMaxInboundMessageSize() {
    return maxInboundMessageSize;
  }

  /**
   * Gets the maximum allowed message size acceptable to send the remote peer.
   */
  @Nullable
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
  public Integer getMaxOutboundMessageSize() {
    return maxOutboundMessageSize;
  }

  /**
   * Copy CallOptions.
   */
  private static Builder toBuilder(CallOptions other) {
    Builder builder = new Builder();
    builder.deadline = other.deadline;
    builder.executor = other.executor;
    builder.authority = other.authority;
    builder.credentials = other.credentials;
    builder.compressorName = other.compressorName;
    builder.customOptions = other.customOptions;
    builder.streamTracerFactories = other.streamTracerFactories;
    builder.waitForReady = other.waitForReady;
    builder.maxInboundMessageSize = other.maxInboundMessageSize;
    builder.maxOutboundMessageSize = other.maxOutboundMessageSize;
    builder.onReadyThreshold = other.onReadyThreshold;
    return builder;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("deadline", deadline)
        .add("authority", authority)
        .add("callCredentials", credentials)
        .add("executor", executor != null ? executor.getClass() : null)
        .add("compressorName", compressorName)
        .add("customOptions", Arrays.deepToString(customOptions))
        .add("waitForReady", isWaitForReady())
        .add("maxInboundMessageSize", maxInboundMessageSize)
        .add("maxOutboundMessageSize", maxOutboundMessageSize)
        .add("onReadyThreshold", onReadyThreshold)
        .add("streamTracerFactories", streamTracerFactories)
        .toString();
  }
}
