/*
 * Copyright 2014 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.checkNotNull;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Metadata.TrustedAsciiMarshaller;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
 * Defines the status of an operation by providing a standard {@link Code} in conjunction with an
 * optional descriptive message. Instances of {@code Status} are created by starting with the
 * template for the appropriate {@link Status.Code} and supplementing it with additional
 * information: {@code Status.NOT_FOUND.withDescription("Could not find 'important_file.txt'");}
 *
 * <p>For clients, every remote call will return a status on completion. In the case of errors this
 * status may be propagated to blocking stubs as a {@link RuntimeException} or to a listener as an
 * explicit parameter.
 *
 * <p>Similarly servers can report a status by throwing {@link StatusRuntimeException}
 * or by passing the status to a callback.
 *
 * <p>Utility functions are provided to convert a status to an exception and to extract them
 * back out.
 *
 * <p>Extended descriptions, including a list of codes that should not be generated by the library,
 * can be found at
 * <a href="https://github.com/grpc/grpc/blob/master/doc/statuscodes.md">doc/statuscodes.md</a>
 */
@Immutable
@CheckReturnValue
public final class Status {

  /**
   * The set of canonical status codes. If new codes are added over time they must choose
   * a numerical value that does not collide with any previously used value.
   */
  public enum Code {
    /**
     * The operation completed successfully.
     */
    OK(0),

    /**
     * The operation was cancelled (typically by the caller).
     */
    CANCELLED(1),

    /**
     * Unknown error.  An example of where this error may be returned is
     * if a Status value received from another address space belongs to
     * an error-space that is not known in this address space.  Also
     * errors raised by APIs that do not return enough error information
     * may be converted to this error.
     */
    UNKNOWN(2),

    /**
     * Client specified an invalid argument.  Note that this differs
     * from FAILED_PRECONDITION.  INVALID_ARGUMENT indicates arguments
     * that are problematic regardless of the state of the system
     * (e.g., a malformed file name).
     */
    INVALID_ARGUMENT(3),

    /**
     * Deadline expired before operation could complete.  For operations
     * that change the state of the system, this error may be returned
     * even if the operation has completed successfully.  For example, a
     * successful response from a server could have been delayed long
     * enough for the deadline to expire.
     */
    DEADLINE_EXCEEDED(4),

    /**
     * Some requested entity (e.g., file or directory) was not found.
     */
    NOT_FOUND(5),

    /**
     * Some entity that we attempted to create (e.g., file or directory) already exists.
     */
    ALREADY_EXISTS(6),

    /**
     * The caller does not have permission to execute the specified
     * operation.  PERMISSION_DENIED must not be used for rejections
     * caused by exhausting some resource (use RESOURCE_EXHAUSTED
     * instead for those errors).  PERMISSION_DENIED must not be
     * used if the caller cannot be identified (use UNAUTHENTICATED
     * instead for those errors).
     */
    PERMISSION_DENIED(7),

    /**
     * Some resource has been exhausted, perhaps a per-user quota, or
     * perhaps the entire file system is out of space.
     */
    RESOURCE_EXHAUSTED(8),

    /**
     * Operation was rejected because the system is not in a state
     * required for the operation's execution.  For example, directory
     * to be deleted may be non-empty, an rmdir operation is applied to
     * a non-directory, etc.
     *
     * <p>A litmus test that may help a service implementor in deciding
     * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE:
     * (a) Use UNAVAILABLE if the client can retry just the failing call.
     * (b) Use ABORTED if the client should retry at a higher-level
     * (e.g., restarting a read-modify-write sequence).
     * (c) Use FAILED_PRECONDITION if the client should not retry until
     * the system state has been explicitly fixed.  E.g., if an "rmdir"
     * fails because the directory is non-empty, FAILED_PRECONDITION
     * should be returned since the client should not retry unless
     * they have first fixed up the directory by deleting files from it.
     */
    FAILED_PRECONDITION(9),

    /**
     * The operation was aborted, typically due to a concurrency issue
     * like sequencer check failures, transaction aborts, etc.
     *
     * <p>See litmus test above for deciding between FAILED_PRECONDITION,
     * ABORTED, and UNAVAILABLE.
     */
    ABORTED(10),

    /**
     * Operation was attempted past the valid range.  E.g., seeking or
     * reading past end of file.
     *
     * <p>Unlike INVALID_ARGUMENT, this error indicates a problem that may
     * be fixed if the system state changes. For example, a 32-bit file
     * system will generate INVALID_ARGUMENT if asked to read at an
     * offset that is not in the range [0,2^32-1], but it will generate
     * OUT_OF_RANGE if asked to read from an offset past the current
     * file size.
     *
     * <p>There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE.
     * We recommend using OUT_OF_RANGE (the more specific error) when it applies
     * so that callers who are iterating through
     * a space can easily look for an OUT_OF_RANGE error to detect when they are done.
     */
    OUT_OF_RANGE(11),

    /**
     * Operation is not implemented or not supported/enabled in this service.
     */
    UNIMPLEMENTED(12),

    /**
     * Internal errors.  Means some invariants expected by underlying
     * system has been broken.  If you see one of these errors,
     * something is very broken.
     */
    INTERNAL(13),

    /**
     * The service is currently unavailable.  This is a most likely a
     * transient condition and may be corrected by retrying with
     * a backoff. Note that it is not always safe to retry
     * non-idempotent operations.
     *
     * <p>See litmus test above for deciding between FAILED_PRECONDITION,
     * ABORTED, and UNAVAILABLE.
     */
    UNAVAILABLE(14),

    /**
     * Unrecoverable data loss or corruption.
     */
    DATA_LOSS(15),

    /**
     * The request does not have valid authentication credentials for the
     * operation.
     */
    UNAUTHENTICATED(16);

    private final int value;
    @SuppressWarnings("ImmutableEnumChecker") // we make sure the byte[] can't be modified
    private final byte[] valueAscii;

    private Code(int value) {
      this.value = value;
      this.valueAscii = Integer.toString(value).getBytes(US_ASCII);
    }

    /**
     * The numerical value of the code.
     */
    public int value() {
      return value;
    }

    /**
     * Returns a {@link Status} object corresponding to this status code.
     */
    public Status toStatus() {
      return STATUS_LIST.get(value);
    }

    private byte[] valueAscii() {
      return valueAscii;
    }
  }

  // Create the canonical list of Status instances indexed by their code values.
  private static final List<Status> STATUS_LIST = buildStatusList();

  private static List<Status> buildStatusList() {
    TreeMap<Integer, Status> canonicalizer = new TreeMap<>();
    for (Code code : Code.values()) {
      Status replaced = canonicalizer.put(code.value(), new Status(code));
      if (replaced != null) {
        throw new IllegalStateException("Code value duplication between "
            + replaced.getCode().name() + " & " + code.name());
      }
    }
    return Collections.unmodifiableList(new ArrayList<>(canonicalizer.values()));
  }

  // A pseudo-enum of Status instances mapped 1:1 with values in Code. This simplifies construction
  // patterns for derived instances of Status.
  /** The operation completed successfully. */
  public static final Status OK = Code.OK.toStatus();
  /** The operation was cancelled (typically by the caller). */
  public static final Status CANCELLED = Code.CANCELLED.toStatus();
  /** Unknown error. See {@link Code#UNKNOWN}. */
  public static final Status UNKNOWN = Code.UNKNOWN.toStatus();
  /** Client specified an invalid argument. See {@link Code#INVALID_ARGUMENT}. */
  public static final Status INVALID_ARGUMENT = Code.INVALID_ARGUMENT.toStatus();
  /** Deadline expired before operation could complete. See {@link Code#DEADLINE_EXCEEDED}. */
  public static final Status DEADLINE_EXCEEDED = Code.DEADLINE_EXCEEDED.toStatus();
  /** Some requested entity (e.g., file or directory) was not found. */
  public static final Status NOT_FOUND = Code.NOT_FOUND.toStatus();
  /** Some entity that we attempted to create (e.g., file or directory) already exists. */
  public static final Status ALREADY_EXISTS = Code.ALREADY_EXISTS.toStatus();
  /**
   * The caller does not have permission to execute the specified operation. See {@link
   * Code#PERMISSION_DENIED}.
   */
  public static final Status PERMISSION_DENIED = Code.PERMISSION_DENIED.toStatus();
  /** The request does not have valid authentication credentials for the operation. */
  public static final Status UNAUTHENTICATED = Code.UNAUTHENTICATED.toStatus();
  /**
   * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
   * is out of space.
   */
  public static final Status RESOURCE_EXHAUSTED = Code.RESOURCE_EXHAUSTED.toStatus();
  /**
   * Operation was rejected because the system is not in a state required for the operation's
   * execution. See {@link Code#FAILED_PRECONDITION}.
   */
  public static final Status FAILED_PRECONDITION =
      Code.FAILED_PRECONDITION.toStatus();
  /**
   * The operation was aborted, typically due to a concurrency issue like sequencer check failures,
   * transaction aborts, etc. See {@link Code#ABORTED}.
   */
  public static final Status ABORTED = Code.ABORTED.toStatus();
  /** Operation was attempted past the valid range. See {@link Code#OUT_OF_RANGE}. */
  public static final Status OUT_OF_RANGE = Code.OUT_OF_RANGE.toStatus();
  /** Operation is not implemented or not supported/enabled in this service. */
  public static final Status UNIMPLEMENTED = Code.UNIMPLEMENTED.toStatus();
  /** Internal errors. See {@link Code#INTERNAL}. */
  public static final Status INTERNAL = Code.INTERNAL.toStatus();
  /** The service is currently unavailable. See {@link Code#UNAVAILABLE}. */
  public static final Status UNAVAILABLE = Code.UNAVAILABLE.toStatus();
  /** Unrecoverable data loss or corruption. */
  public static final Status DATA_LOSS = Code.DATA_LOSS.toStatus();

  /**
   * Return a {@link Status} given a canonical error {@link Code} value.
   */
  public static Status fromCodeValue(int codeValue) {
    if (codeValue < 0 || codeValue >= STATUS_LIST.size()) {
      return UNKNOWN.withDescription("Unknown code " + codeValue);
    } else {
      return STATUS_LIST.get(codeValue);
    }
  }

  private static Status fromCodeValue(byte[] asciiCodeValue) {
    if (asciiCodeValue.length == 1 && asciiCodeValue[0] == '0') {
      return Status.OK;
    }
    return fromCodeValueSlow(asciiCodeValue);
  }

  @SuppressWarnings("fallthrough")
  private static Status fromCodeValueSlow(byte[] asciiCodeValue) {
    int index = 0;
    int codeValue = 0;
    switch (asciiCodeValue.length) {
      case 2:
        if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') {
          break;
        }
        codeValue += (asciiCodeValue[index++] - '0') * 10;
        // fall through
      case 1:
        if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') {
          break;
        }
        codeValue += asciiCodeValue[index] - '0';
        if (codeValue < STATUS_LIST.size()) {
          return STATUS_LIST.get(codeValue);
        }
        break;
      default:
        break;
    }
    return UNKNOWN.withDescription("Unknown code " + new String(asciiCodeValue, US_ASCII));
  }

  /**
   * Return a {@link Status} given a canonical error {@link Code} object.
   */
  public static Status fromCode(Code code) {
    return code.toStatus();
  }

  /**
   * Key to bind status code to trailing metadata.
   */
  static final Metadata.Key<Status> CODE_KEY
      = Metadata.Key.of("grpc-status", false /* not pseudo */, new StatusCodeMarshaller());

  /**
   * Marshals status messages for ({@link #MESSAGE_KEY}.  gRPC does not use binary coding of
   * status messages by default, which makes sending arbitrary strings difficult.  This marshaller
   * uses ASCII printable characters by default, and percent encodes (e.g. %0A) all non ASCII bytes.
   * This leads to normal text being mostly readable (especially useful for debugging), and special
   * text still being sent.
   *
   * <p>By default, the HTTP spec says that header values must be encoded using a strict subset of
   * ASCII (See RFC 7230 section 3.2.6).  HTTP/2 HPACK allows use of arbitrary binary headers, but
   * we do not use them for interoperating with existing HTTP/1.1 code.  Since the grpc-message
   * is encoded to such a header, it needs to not use forbidden characters.
   *
   * <p>This marshaller works by converting the passed in string into UTF-8, checking to see if
   * each individual byte is an allowable byte, and then either percent encoding or passing it
   * through.  When percent encoding, the byte is converted into hexadecimal notation with a '%'
   * prepended.
   *
   * <p>When unmarshalling, bytes are passed through unless they match the "%XX" pattern.  If they
   * do match, the unmarshaller attempts to convert them back into their original UTF-8 byte
   * sequence.  After the input header bytes are converted into UTF-8 bytes, the new byte array is
   * reinterpretted back as a string.
   */
  private static final TrustedAsciiMarshaller<String> STATUS_MESSAGE_MARSHALLER =
      new StatusMessageMarshaller();

  /**
   * Key to bind status message to trailing metadata.
   */
  static final Metadata.Key<String> MESSAGE_KEY =
      Metadata.Key.of("grpc-message", false /* not pseudo */, STATUS_MESSAGE_MARSHALLER);

  /**
   * Extract an error {@link Status} from the causal chain of a {@link Throwable}.
   * If no status can be found, a status is created with {@link Code#UNKNOWN} as its code and
   * {@code t} as its cause.
   *
   * @return non-{@code null} status
   */
  public static Status fromThrowable(Throwable t) {
    Throwable cause = checkNotNull(t, "t");
    while (cause != null) {
      if (cause instanceof StatusException) {
        return ((StatusException) cause).getStatus();
      } else if (cause instanceof StatusRuntimeException) {
        return ((StatusRuntimeException) cause).getStatus();
      }
      cause = cause.getCause();
    }
    // Couldn't find a cause with a Status
    return UNKNOWN.withCause(t);
  }

  /**
   * Extract an error trailers from the causal chain of a {@link Throwable}.
   *
   * @return the trailers or {@code null} if not found.
   */
  @Nullable
  public static Metadata trailersFromThrowable(Throwable t) {
    Throwable cause = checkNotNull(t, "t");
    while (cause != null) {
      if (cause instanceof StatusException) {
        return ((StatusException) cause).getTrailers();
      } else if (cause instanceof StatusRuntimeException) {
        return ((StatusRuntimeException) cause).getTrailers();
      }
      cause = cause.getCause();
    }
    return null;
  }

  static String formatThrowableMessage(Status status) {
    if (status.description == null) {
      return status.code.toString();
    } else {
      return status.code + ": " + status.description;
    }
  }

  private final Code code;
  private final String description;
  private final Throwable cause;

  private Status(Code code) {
    this(code, null, null);
  }

  private Status(Code code, @Nullable String description, @Nullable Throwable cause) {
    this.code = checkNotNull(code, "code");
    this.description = description;
    this.cause = cause;
  }

  /**
   * Create a derived instance of {@link Status} with the given cause.
   * However, the cause is not transmitted from server to client.
   */
  public Status withCause(Throwable cause) {
    if (Objects.equal(this.cause, cause)) {
      return this;
    }
    return new Status(this.code, this.description, cause);
  }

  /**
   * Create a derived instance of {@link Status} with the given description.  Leading and trailing
   * whitespace may be removed; this may change in the future.
   */
  public Status withDescription(String description) {
    if (Objects.equal(this.description, description)) {
      return this;
    }
    return new Status(this.code, description, this.cause);
  }

  /**
   * Create a derived instance of {@link Status} augmenting the current description with
   * additional detail.  Leading and trailing whitespace may be removed; this may change in the
   * future.
   */
  public Status augmentDescription(String additionalDetail) {
    if (additionalDetail == null) {
      return this;
    } else if (this.description == null) {
      return new Status(this.code, additionalDetail, this.cause);
    } else {
      return new Status(this.code, this.description + "\n" + additionalDetail, this.cause);
    }
  }

  /**
   * The canonical status code.
   */
  public Code getCode() {
    return code;
  }

  /**
   * A description of this status for human consumption.
   */
  @Nullable
  public String getDescription() {
    return description;
  }

  /**
   * The underlying cause of an error.
   * Note that the cause is not transmitted from server to client.
   */
  @Nullable
  public Throwable getCause() {
    return cause;
  }

  /**
   * Is this status OK, i.e., not an error.
   */
  public boolean isOk() {
    return Code.OK == code;
  }

  /**
   * Convert this {@link Status} to a {@link RuntimeException}. Use {@link #fromThrowable}
   * to recover this {@link Status} instance when the returned exception is in the causal chain.
   */
  public StatusRuntimeException asRuntimeException() {
    return new StatusRuntimeException(this);
  }

  /**
   * Same as {@link #asRuntimeException()} but includes the provided trailers in the returned
   * exception.
   */
  public StatusRuntimeException asRuntimeException(@Nullable Metadata trailers) {
    return new StatusRuntimeException(this, trailers);
  }

  /**
   * Convert this {@link Status} to an {@link Exception}. Use {@link #fromThrowable}
   * to recover this {@link Status} instance when the returned exception is in the causal chain.
   */
  public StatusException asException() {
    return new StatusException(this);
  }

  /**
   * Same as {@link #asException()} but includes the provided trailers in the returned exception.
   */
  public StatusException asException(@Nullable Metadata trailers) {
    return new StatusException(this, trailers);
  }

  /** A string representation of the status useful for debugging. */
  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("code", code.name())
        .add("description", description)
        .add("cause", cause != null ? getStackTraceAsString(cause) : cause)
        .toString();
  }

  private static final class StatusCodeMarshaller implements TrustedAsciiMarshaller<Status> {
    @Override
    public byte[] toAsciiString(Status status) {
      return status.getCode().valueAscii();
    }

    @Override
    public Status parseAsciiString(byte[] serialized) {
      return fromCodeValue(serialized);
    }
  }

  private static final class StatusMessageMarshaller implements TrustedAsciiMarshaller<String> {

    private static final byte[] HEX =
        {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    @Override
    public byte[] toAsciiString(String value) {
      byte[] valueBytes = value.getBytes(UTF_8);
      for (int i = 0; i < valueBytes.length; i++) {
        byte b = valueBytes[i];
        // If there are only non escaping characters, skip the slow path.
        if (isEscapingChar(b)) {
          return toAsciiStringSlow(valueBytes, i);
        }
      }
      return valueBytes;
    }

    private static boolean isEscapingChar(byte b) {
      return b < ' ' || b >= '~' || b == '%';
    }

    /**
     * Percent encode bytes to make them ASCII.
     *
     * @param valueBytes the UTF-8 bytes
     * @param ri The reader index, pointed at the first byte that needs escaping.
     */
    private static byte[] toAsciiStringSlow(byte[] valueBytes, int ri) {
      byte[] escapedBytes = new byte[ri + (valueBytes.length - ri) * 3];
      // copy over the good bytes
      if (ri != 0) {
        System.arraycopy(valueBytes, 0, escapedBytes, 0, ri);
      }
      int wi = ri;
      for (; ri < valueBytes.length; ri++) {
        byte b = valueBytes[ri];
        // Manually implement URL encoding, per the gRPC spec.
        if (isEscapingChar(b)) {
          escapedBytes[wi] = '%';
          escapedBytes[wi + 1] = HEX[(b >> 4) & 0xF];
          escapedBytes[wi + 2] = HEX[b & 0xF];
          wi += 3;
          continue;
        }
        escapedBytes[wi++] = b;
      }
      return Arrays.copyOf(escapedBytes, wi);
    }

    @SuppressWarnings("deprecation") // Use fast but deprecated String ctor
    @Override
    public String parseAsciiString(byte[] value) {
      for (int i = 0; i < value.length; i++) {
        byte b = value[i];
        if (b < ' ' || b >= '~' || (b == '%' && i + 2 < value.length)) {
          return parseAsciiStringSlow(value);
        }
      }
      return new String(value, 0);
    }

    private static String parseAsciiStringSlow(byte[] value) {
      ByteBuffer buf = ByteBuffer.allocate(value.length);
      for (int i = 0; i < value.length;) {
        if (value[i] == '%' && i + 2 < value.length) {
          try {
            buf.put((byte)Integer.parseInt(new String(value, i + 1, 2, US_ASCII), 16));
            i += 3;
            continue;
          } catch (NumberFormatException e) {
            // ignore, fall through, just push the bytes.
          }
        }
        buf.put(value[i]);
        i += 1;
      }
      return new String(buf.array(), 0, buf.position(), UTF_8);
    }
  }

  /**
   * Equality on Statuses is not well defined.  Instead, do comparison based on their Code with
   * {@link #getCode}.  The description and cause of the Status are unlikely to be stable, and
   * additional fields may be added to Status in the future.
   */
  @Override
  public boolean equals(Object obj) {
    return super.equals(obj);
  }

  /**
   * Hash codes on Statuses are not well defined.
   *
   * @see #equals
   */
  @Override
  public int hashCode() {
    return super.hashCode();
  }
}
