// Copyright 2014 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.shell;

import java.time.Duration;
import java.util.Optional;

/**
 * Represents the termination status of a command.  {@link Process#waitFor} is
 * not very precisely specified, so this class encapsulates the interpretation
 * of values returned by it.
 *
 * Caveat: due to the lossy encoding, it's not always possible to accurately
 * distinguish signal and exit cases.  In particular, processes that exit with
 * a value within the interval [129, 191] will be mistaken for having been
 * terminated by a signal.
 *
 * Instances are immutable.
 */
public final class TerminationStatus {

  private final int waitResult;
  private final boolean timedOut;
  private final Optional<Duration> wallExecutionTime;
  private final Optional<Duration> userExecutionTime;
  private final Optional<Duration> systemExecutionTime;

  /**
   * Values taken from the glibc strsignal(3) function.
   */
  private static final String[] SIGNAL_STRINGS = {
    null,
    "Hangup",
    "Interrupt",
    "Quit",
    "Illegal instruction",
    "Trace/breakpoint trap",
    "Aborted",
    "Bus error",
    "Floating point exception",
    "Killed",
    "User defined signal 1",
    "Segmentation fault",
    "User defined signal 2",
    "Broken pipe",
    "Alarm clock",
    "Terminated",
    "Stack fault",
    "Child exited",
    "Continued",
    "Stopped (signal)",
    "Stopped",
    "Stopped (tty input)",
    "Stopped (tty output)",
    "Urgent I/O condition",
    "CPU time limit exceeded",
    "File size limit exceeded",
    "Virtual timer expired",
    "Profiling timer expired",
    "Window changed",
    "I/O possible",
    "Power failure",
    "Bad system call",
  };

  private static String getSignalString(int signum) {
    return signum > 0 && signum < SIGNAL_STRINGS.length
        ? SIGNAL_STRINGS[signum]
        : "Signal " + signum;
  }

  /**
   * Construct a TerminationStatus instance from a Process waitFor code.
   *
   * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
   * @param timedOut whether the execution timed out
   */
  public TerminationStatus(int waitResult, boolean timedOut) {
    this(waitResult, timedOut, Optional.empty(), Optional.empty(), Optional.empty());
  }

  /**
   * Construct a TerminationStatus instance from a Process waitFor code.
   *
   * <p>TerminationStatus objects are considered equal if they have the same waitResult.
   *
   * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
   * @param timedOut whether the execution timed out
   * @param wallExecutionTime the wall execution time of the command, if available
   * @param userExecutionTime the user execution time of the command, if available
   * @param systemExecutionTime the system execution time of the command, if available
   */
  public TerminationStatus(
      int waitResult,
      boolean timedOut,
      Optional<Duration> wallExecutionTime,
      Optional<Duration> userExecutionTime,
      Optional<Duration> systemExecutionTime) {
    this.waitResult = waitResult;
    this.timedOut = timedOut;
    this.wallExecutionTime = wallExecutionTime;
    this.userExecutionTime = userExecutionTime;
    this.systemExecutionTime = systemExecutionTime;
  }

  /**
   * Returns the exit code returned by the subprocess.
   */
  public int getRawExitCode() {
    return waitResult;
  }

  /**
   * Returns true iff the process exited with code 0.
   */
  public boolean success() {
    return exited() && getExitCode() == 0;
  }

  // We're relying on undocumented behaviour of Process.waitFor, specifically
  // that waitResult is the exit status when the process returns normally, or
  // 128+signalnumber when the process is terminated by a signal.  We further
  // assume that value signal numbers fall in the interval [1, 63].
  private static final int SIGNAL_1  = 128 + 1;
  private static final int SIGNAL_63 = 128 + 63;

  /**
   * Returns true iff the process exited normally.
   */
  public boolean exited() {
    return !timedOut && (waitResult < SIGNAL_1 || waitResult > SIGNAL_63);
  }

  /** Returns true if the process timed out. */
  public boolean timedOut() {
    return timedOut;
  }

  /**
   * Returns the exit code of the subprocess.  Undefined if exited() is false.
   */
  public int getExitCode() {
    if (!exited()) {
      throw new IllegalStateException("getExitCode() not defined");
    }
    return waitResult;
  }

  /**
   * Returns the number of the signal that terminated the process.  Undefined
   * if exited() returns true.
   */
  public int getTerminatingSignal() {
    if (exited() || timedOut) {
      throw new IllegalStateException("getTerminatingSignal() not defined");
    }
    return waitResult - SIGNAL_1 + 1;
  }

  /**
   * Returns the wall execution time.
   *
   * @return the measurement, or empty in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  public Optional<Duration> getWallExecutionTime() {
    return wallExecutionTime;
  }

  /**
   * Returns the user execution time.
   *
   * @return the measurement, or empty in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  public Optional<Duration> getUserExecutionTime() {
    return userExecutionTime;
  }

  /**
   * Returns the system execution time.
   *
   * @return the measurement, or empty in case of execution errors or when the measurement is not
   *     implemented for the current platform
   */
  public Optional<Duration> getSystemExecutionTime() {
    return systemExecutionTime;
  }

  /**
   * Returns a short string describing the termination status.
   * e.g. "Exit 1" or "Hangup".
   */
  public String toShortString() {
    return exited()
        ? "Exit " + getExitCode()
        : (timedOut ? "Timeout" : getSignalString(getTerminatingSignal()));
  }

  @Override
  public String toString() {
    if (exited()) {
      return "Process exited with status " + getExitCode();
    } else if (timedOut) {
      return "Timed out";
    } else {
      return "Process terminated by signal " + getTerminatingSignal();
    }
  }

  @Override
  public int hashCode() {
    return waitResult;
  }

  @Override
  public boolean equals(Object other) {
    return other instanceof TerminationStatus
        && ((TerminationStatus) other).waitResult == this.waitResult;
  }

  /** Returns a new {@link TerminationStatus.Builder}. */
  public static Builder builder() {
    return new TerminationStatus.Builder();
  }

  /** Builder for {@link TerminationStatus} objects. */
  public static class Builder {
    // We use nullness here instead of Optional to avoid confusion between fields that may not
    // yet have been set from fields that can legitimately hold an Optional value in the built
    // object.

    private Integer waitResponse = null;
    private Boolean timedOut = null;

    private Optional<Duration> wallExecutionTime = Optional.empty();
    private Optional<Duration> userExecutionTime = Optional.empty();
    private Optional<Duration> systemExecutionTime = Optional.empty();

    /** Sets the value returned by {@link java.lang.Process#waitFor}. */
    public Builder setWaitResponse(int waitResponse) {
      this.waitResponse = waitResponse;
      return this;
    }

    /** Sets whether the action timed out or not. */
    public Builder setTimedOut(boolean timedOut) {
      this.timedOut = timedOut;
      return this;
    }

    /** Sets the wall execution time. */
    public Builder setWallExecutionTime(Duration wallExecutionTime) {
      this.wallExecutionTime = Optional.of(wallExecutionTime);
      return this;
    }

    /** Sets the user execution time. */
    public Builder setUserExecutionTime(Duration userExecutionTime) {
      this.userExecutionTime = Optional.of(userExecutionTime);
      return this;
    }

    /** Sets the system execution time. */
    public Builder setSystemExecutionTime(Duration systemExecutionTime) {
      this.systemExecutionTime = Optional.of(systemExecutionTime);
      return this;
    }

    /** Sets or clears the wall execution time. */
    public Builder setWallExecutionTime(Optional<Duration> wallExecutionTime) {
      this.wallExecutionTime = wallExecutionTime;
      return this;
    }

    /** Sets or clears the user execution time. */
    public Builder setUserExecutionTime(Optional<Duration> userExecutionTime) {
      this.userExecutionTime = userExecutionTime;
      return this;
    }

    /** Sets or clears the system execution time. */
    public Builder setSystemExecutionTime(Optional<Duration> systemExecutionTime) {
      this.systemExecutionTime = systemExecutionTime;
      return this;
    }

    /** Builds a {@link TerminationStatus} object. */
    public TerminationStatus build() {
      if (waitResponse == null) {
        throw new IllegalStateException("waitResponse never set");
      }
      if (timedOut == null) {
        throw new IllegalStateException("timedOut never set");
      }
      return new TerminationStatus(
          waitResponse, timedOut, wallExecutionTime, userExecutionTime, systemExecutionTime);
    }
  }
}
