package com.google.inject.spi;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.inject.internal.Messages;
import java.io.Serializable;
import java.util.Formatter;
import java.util.List;
import java.util.Optional;

/**
 * Details about a single Guice error and supports formatting itself in the context of other Guice
 * errors.
 *
 * <p>WARNING: The class and its APIs are still experimental and subject to change.
 *
 * @since 5.0
 */
public abstract class ErrorDetail<SelfT extends ErrorDetail<SelfT>> implements Serializable {
  private final String message;
  private final ImmutableList<Object> sources;
  private final Throwable cause;

  protected ErrorDetail(String message, List<Object> sources, Throwable cause) {
    this.message = message;
    this.sources = ImmutableList.copyOf(sources);
    this.cause = cause;
  }

  /**
   * Returns true if this error can be merged with the {@code otherError} and formatted together.
   *
   * <p>By default this return false and implementations that support merging with other errors
   * should override this method.
   */
  public boolean isMergeable(ErrorDetail<?> otherError) {
    return false;
  }

  /**
   * Formats this error along with other errors that are mergeable with this error.
   *
   * <p>{@code mergeableErrors} is a list that contains all other errors that are reported in the
   * same exception that are considered to be mergable with this error base on result of calling
   * {@link #isMergeable}. The list will be empty if non of the other errors are mergable with this
   * error.
   *
   * <p>Formatted error has the following structure:
   *
   * <ul>
   *   <li>Summary of the error
   *   <li>Details about the error such as the source of the error
   *   <li>Hints for fixing the error if available
   *   <li>Link to the documentation on this error in greater detail
   *
   * @param index index for this error
   * @param mergeableErrors list of errors that are mergeable with this error
   * @param formatter for printing the error message
   */
  public final void format(int index, List<ErrorDetail<?>> mergeableErrors, Formatter formatter) {
    String id = getErrorIdentifier().map(s -> "[" + Messages.redBold(s) + "]: ").orElse("");
    formatter.format("%s) %s%s%n", index, id, getMessage());
    formatDetail(mergeableErrors, formatter);
    // TODO(b/151482394): Output potiential fixes for the error
    Optional<String> learnMoreLink = getLearnMoreLink();
    if (learnMoreLink.isPresent()) {
      formatter.format("%n%s%n", Messages.bold("Learn more:"));
      formatter.format("  %s%n", Messages.underline(learnMoreLink.get()));
    }
  }

  /**
   * Formats the detail of this error message along with other errors that are mergeable with this
   * error. This is called from {@link #format}.
   *
   * <p>{@code mergeableErrors} is a list that contains all other errors that are reported in the
   * same exception that are considered to be mergable with this error base on result of calling
   * {@link #isMergeable}. The list will be empty if non of the other errors are mergable with this
   * error.
   *
   * @param mergeableErrors list of errors that are mergeable with this error
   * @param formatter for printing the error message
   */
  protected abstract void formatDetail(List<ErrorDetail<?>> mergeableErrors, Formatter formatter);

  /**
   * Returns an optional link to additional documentation about this error to be included in the
   * formatted error message.
   */
  protected Optional<String> getLearnMoreLink() {
    return Optional.empty();
  }

  /** Returns an optional string identifier for this error. */
  protected Optional<String> getErrorIdentifier() {
    return Optional.empty();
  }

  public String getMessage() {
    return message;
  }

  public List<Object> getSources() {
    return sources;
  }

  public Throwable getCause() {
    return cause;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(message, cause, sources);
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof ErrorDetail)) {
      return false;
    }
    ErrorDetail<?> e = (ErrorDetail<?>) o;
    return message.equals(e.message) && Objects.equal(cause, e.cause) && sources.equals(e.sources);
  }

  /** Returns a new instance of the same {@link ErrorDetail} with updated sources. */
  public abstract SelfT withSources(List<Object> newSources);
}