package com.bumptech.glide.load.engine;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Key;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * An exception with zero or more causes indicating why a load in Glide failed.
 */
public final class GlideException extends Exception {
  private static final StackTraceElement[] EMPTY_ELEMENTS = new StackTraceElement[0];

  private final List<Exception> causes;
  private Key key;
  private DataSource dataSource;
  private Class<?> dataClass;

  public GlideException(String message) {
    this(message, Collections.<Exception>emptyList());
  }

  public GlideException(String detailMessage, Exception cause) {
    this(detailMessage, Collections.singletonList(cause));
  }

  public GlideException(String detailMessage, List<Exception> causes) {
    super(detailMessage);
    setStackTrace(EMPTY_ELEMENTS);
    this.causes = causes;
  }

  void setLoggingDetails(Key key, DataSource dataSource) {
    setLoggingDetails(key, dataSource, null);
  }

  void setLoggingDetails(Key key, DataSource dataSource, Class<?> dataClass) {
    this.key = key;
    this.dataSource = dataSource;
    this.dataClass = dataClass;
  }

  @Override
  public Throwable fillInStackTrace() {
    // Avoid an expensive allocation by doing nothing here. Causes should contain all relevant
    // stack traces.
    return this;
  }

  /**
   * Returns a list of causes that are immediate children of this exception.
   *
   * <p>Causes may or may not be {@link GlideException GlideExceptions}. Causes may also not be root
   * causes, and in turn my have been caused by other failures.</p>
   *
   * @see #getRootCauses()
   */
  public List<Exception> getCauses() {
    return causes;
  }

  /**
   * Returns the list of root causes that are the leaf nodes of all children of this exception.
   *
   * <p>Use this method to do things like look for http exceptions that indicate the load may have
   * failed due to an error that can be retried. Keep in mind that because Glide may attempt to load
   * a given model using multiple different pathways, there may be multiple related or unrelated
   * reasons for a load to fail.
   */
  public List<Exception> getRootCauses() {
    List<Exception> rootCauses = new ArrayList<>();
    addRootCauses(this, rootCauses);
    return rootCauses;
  }

  /**
   * Logs all root causes using the given tag.
   *
   * <p>Each root cause is logged separately to avoid throttling. {@link #printStackTrace()} will
   * provide a more succinct overview of why the exception occurred, although it does not include
   * complete stack traces.
   */
  public void logRootCauses(String tag) {
    List<Exception> causes = getRootCauses();
    for (int i = 0, size = causes.size(); i < size; i++) {
      Log.i(tag, "Root cause (" + (i + 1) + " of " + size + ")", causes.get(i));
    }
  }

  private void addRootCauses(Exception exception, List<Exception> rootCauses) {
    if (exception instanceof GlideException) {
      GlideException glideException = (GlideException) exception;
      for (Exception e : glideException.getCauses()) {
        addRootCauses(e, rootCauses);
      }
    } else {
      rootCauses.add(exception);
    }
  }

  @Override
  public void printStackTrace() {
    printStackTrace(System.err);
  }

  @Override
  public void printStackTrace(PrintStream err) {
    printStackTrace((Appendable) err);
  }

  @Override
  public void printStackTrace(PrintWriter err) {
    printStackTrace((Appendable) err);
  }

  private void printStackTrace(Appendable appendable) {
    appendExceptionMessage(this, appendable);
    appendCauses(getCauses(), new IndentedAppendable(appendable));
  }

  @Override
  public String getMessage() {
    return super.getMessage()
        + (dataClass != null ? ", " + dataClass : "")
        + (dataSource != null ? ", " + dataSource : "")
        + (key != null ? ", " + key : "");
  }

  // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
  // never happen.
  @SuppressWarnings("PMD.PreserveStackTrace")
  private static void appendExceptionMessage(Exception e, Appendable appendable) {
    try {
      appendable.append(e.getClass().toString()).append(": ").append(e.getMessage()).append('\n');
    } catch (IOException e1) {
      throw new RuntimeException(e);
    }
  }

  // Appendable throws, PrintWriter, PrintStream, and IndentedAppendable do not, so this should
  // never happen.
  @SuppressWarnings("PMD.PreserveStackTrace")
  private static void appendCauses(List<Exception> causes, Appendable appendable) {
    try {
      appendCausesWrapped(causes, appendable);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
  private static void appendCausesWrapped(List<Exception> causes, Appendable appendable)
      throws IOException {
    int size = causes.size();
    for (int i = 0; i < size; i++) {
      appendable.append("Cause (")
          .append(String.valueOf(i + 1))
          .append(" of ")
          .append(String.valueOf(size))
          .append("): ");

      Exception cause = causes.get(i);
      if (cause instanceof GlideException) {
        GlideException glideCause = (GlideException) cause;
        glideCause.printStackTrace(appendable);
      } else {
        appendExceptionMessage(cause, appendable);
      }
    }
  }

  private static final class IndentedAppendable implements Appendable {
    private static final String EMPTY_SEQUENCE = "";
    private static final String INDENT = "  ";
    private final Appendable appendable;
    private boolean printedNewLine = true;

    IndentedAppendable(Appendable appendable) {
      this.appendable = appendable;
    }

    @Override
    public Appendable append(char c) throws IOException {
      if (printedNewLine) {
        printedNewLine = false;
        appendable.append(INDENT);
      }
      printedNewLine = c == '\n';
      appendable.append(c);
      return this;
    }

    @Override
    public Appendable append(@Nullable CharSequence charSequence) throws IOException {
      charSequence = safeSequence(charSequence);
      return append(charSequence, 0, charSequence.length());
    }

    @Override
    public Appendable append(@Nullable CharSequence charSequence, int start, int end)
        throws IOException {
      charSequence = safeSequence(charSequence);
      if (printedNewLine) {
        printedNewLine = false;
        appendable.append(INDENT);
      }
      printedNewLine = charSequence.length() > 0 && charSequence.charAt(end - 1) == '\n';
      appendable.append(charSequence, start, end);
      return this;
    }

    @NonNull
    private CharSequence safeSequence(@Nullable CharSequence sequence) {
      if (sequence == null) {
        return EMPTY_SEQUENCE;
      } else {
        return sequence;
      }
    }
  }
}
