/*
 * Copyright 2017, OpenCensus 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.opencensus.contrib.zpages;

import static com.google.common.html.HtmlEscapers.htmlEscaper;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.BaseEncoding;
import io.opencensus.common.Duration;
import io.opencensus.common.Function;
import io.opencensus.common.Functions;
import io.opencensus.common.Timestamp;
import io.opencensus.trace.Annotation;
import io.opencensus.trace.AttributeValue;
import io.opencensus.trace.SpanContext;
import io.opencensus.trace.SpanId;
import io.opencensus.trace.Status;
import io.opencensus.trace.Status.CanonicalCode;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.export.RunningSpanStore;
import io.opencensus.trace.export.SampledSpanStore;
import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
import io.opencensus.trace.export.SpanData;
import io.opencensus.trace.export.SpanData.TimedEvent;
import io.opencensus.trace.export.SpanData.TimedEvents;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

/*>>>
import org.checkerframework.checker.nullness.qual.Nullable;
*/

// TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
/**
 * HTML page formatter for tracing debug. The page displays information about all active spans and
 * all sampled spans based on latency and errors.
 *
 * <p>It prints a summary table which contains one row for each span name and data about number of
 * active and sampled spans.
 */
final class TracezZPageHandler extends ZPageHandler {
  private enum RequestType {
    RUNNING(0),
    FINISHED(1),
    FAILED(2),
    UNKNOWN(-1);

    private final int value;

    RequestType(int value) {
      this.value = value;
    }

    static RequestType fromString(String str) {
      int value = Integer.parseInt(str);
      switch (value) {
        case 0:
          return RUNNING;
        case 1:
          return FINISHED;
        case 2:
          return FAILED;
        default:
          return UNKNOWN;
      }
    }

    int getValue() {
      return value;
    }
  }

  private static final String TRACEZ_URL = "/tracez";
  private static final Tracer tracer = Tracing.getTracer();
  // Color to use for zebra-striping.
  private static final String ZEBRA_STRIPE_COLOR = "#FFF";
  // Color for sampled traceIds.
  private static final String SAMPLED_TRACE_ID_COLOR = "#C1272D";
  // Color for not sampled traceIds
  private static final String NOT_SAMPLED_TRACE_ID_COLOR = "black";
  // The header for span name.
  private static final String HEADER_SPAN_NAME = "zspanname";
  // The header for type (running = 0, latency = 1, error = 2) to display.
  private static final String HEADER_SAMPLES_TYPE = "ztype";
  // The header for sub-type:
  // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one;
  // * for error based samples [0, 15], 0 - means all, otherwise the error code;
  private static final String HEADER_SAMPLES_SUB_TYPE = "zsubtype";
  // Map from LatencyBucketBoundaries to the human string displayed on the UI for each bucket.
  private static final Map<LatencyBucketBoundaries, String> LATENCY_BUCKET_BOUNDARIES_STRING_MAP =
      buildLatencyBucketBoundariesStringMap();
  @javax.annotation.Nullable private final RunningSpanStore runningSpanStore;
  @javax.annotation.Nullable private final SampledSpanStore sampledSpanStore;

  private TracezZPageHandler(
      @javax.annotation.Nullable RunningSpanStore runningSpanStore,
      @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
    this.runningSpanStore = runningSpanStore;
    this.sampledSpanStore = sampledSpanStore;
  }

  /**
   * Constructs a new {@code TracezZPageHandler}.
   *
   * @param runningSpanStore the instance of the {@code RunningSpanStore} to be used.
   * @param sampledSpanStore the instance of the {@code SampledSpanStore} to be used.
   * @return a new {@code TracezZPageHandler}.
   */
  static TracezZPageHandler create(
      @javax.annotation.Nullable RunningSpanStore runningSpanStore,
      @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
    return new TracezZPageHandler(runningSpanStore, sampledSpanStore);
  }

  @Override
  public String getUrlPath() {
    return TRACEZ_URL;
  }

  private static void emitStyle(PrintWriter out) {
    out.write("<style>\n");
    out.write(Style.style);
    out.write("</style>\n");
  }

  @Override
  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
    PrintWriter out =
        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
    out.write("<!DOCTYPE html>\n");
    out.write("<html lang=\"en\"><head>\n");
    out.write("<meta charset=\"utf-8\">\n");
    out.write("<title>TraceZ</title>\n");
    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
    out.write(
        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
            + "rel=\"stylesheet\">\n");
    out.write(
        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
    emitStyle(out);
    out.write("</head>\n");
    out.write("<body>\n");
    out.write(
        "<p class=\"header\">"
            + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
            + "Open<span>Census</span></p>");
    out.write("<h1>TraceZ Summary</h1>\n");

    try {
      emitHtmlBody(queryMap, out);
    } catch (Throwable t) {
      out.write("Errors while generate the HTML page " + t);
    }
    out.write("</body>\n");
    out.write("</html>\n");
    out.close();
  }

  private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out)
      throws UnsupportedEncodingException {
    if (runningSpanStore == null || sampledSpanStore == null) {
      out.write("OpenCensus implementation not available.");
      return;
    }
    Formatter formatter = new Formatter(out, Locale.US);
    emitSummaryTable(out, formatter);
    String spanName = queryMap.get(HEADER_SPAN_NAME);
    if (spanName != null) {
      tracer
          .getCurrentSpan()
          .addAnnotation(
              "Render spans.",
              ImmutableMap.<String, AttributeValue>builder()
                  .put("SpanName", AttributeValue.stringAttributeValue(spanName))
                  .build());
      String typeStr = queryMap.get(HEADER_SAMPLES_TYPE);
      if (typeStr != null) {
        List<SpanData> spans = null;
        RequestType type = RequestType.fromString(typeStr);
        if (type == RequestType.UNKNOWN) {
          return;
        }
        if (type == RequestType.RUNNING) {
          // Display running.
          spans =
              new ArrayList<>(
                  runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create(spanName, 0)));
          // Sort active spans incremental.
          Collections.sort(spans, new SpanDataComparator(/* incremental= */ true));
        } else {
          String subtypeStr = queryMap.get(HEADER_SAMPLES_SUB_TYPE);
          if (subtypeStr != null) {
            int subtype = Integer.parseInt(subtypeStr);
            if (type == RequestType.FAILED) {
              if (subtype < 0 || subtype >= CanonicalCode.values().length) {
                return;
              }
              // Display errors. subtype 0 means all.
              CanonicalCode code = subtype == 0 ? null : CanonicalCode.values()[subtype];
              spans =
                  new ArrayList<>(
                      sampledSpanStore.getErrorSampledSpans(ErrorFilter.create(spanName, code, 0)));
            } else {
              if (subtype < 0 || subtype >= LatencyBucketBoundaries.values().length) {
                return;
              }
              // Display latency.
              LatencyBucketBoundaries latencyBucketBoundaries =
                  LatencyBucketBoundaries.values()[subtype];
              spans =
                  new ArrayList<>(
                      sampledSpanStore.getLatencySampledSpans(
                          LatencyFilter.create(
                              spanName,
                              latencyBucketBoundaries.getLatencyLowerNs(),
                              latencyBucketBoundaries.getLatencyUpperNs(),
                              0)));
              // Sort sampled spans decremental.
              Collections.sort(spans, new SpanDataComparator(/* incremental= */ false));
            }
          }
        }
        emitSpanNameAndCountPages(formatter, spanName, spans == null ? 0 : spans.size(), type);

        if (spans != null) {
          emitSpans(out, formatter, spans);
          emitLegend(out);
        }
      }
    }
  }

  private static void emitSpanNameAndCountPages(
      Formatter formatter, String spanName, int returnedNum, RequestType type) {
    formatter.format("<p><b>Span Name: %s </b></p>%n", htmlEscaper().escape(spanName));
    formatter.format(
        "%s Requests %d</b></p>%n",
        type == RequestType.RUNNING
            ? "Running"
            : type == RequestType.FINISHED ? "Finished" : "Failed",
        returnedNum);
  }

  /** Emits the list of SampledRequets with a header. */
  private static void emitSpans(PrintWriter out, Formatter formatter, Collection<SpanData> spans) {
    out.write("<pre>\n");
    formatter.format("%-23s %18s%n", "When", "Elapsed(s)");
    out.write("-------------------------------------------\n");
    for (SpanData span : spans) {
      tracer
          .getCurrentSpan()
          .addAnnotation(
              "Render span.",
              ImmutableMap.<String, AttributeValue>builder()
                  .put(
                      "SpanId",
                      AttributeValue.stringAttributeValue(
                          BaseEncoding.base16()
                              .lowerCase()
                              .encode(span.getContext().getSpanId().getBytes())))
                  .build());

      emitSingleSpan(formatter, span);
    }
    out.write("</pre>\n");
  }

  // Emits the internal html for a single {@link SpanData}.
  @SuppressWarnings("deprecation")
  private static void emitSingleSpan(Formatter formatter, SpanData span) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(TimeUnit.SECONDS.toMillis(span.getStartTimestamp().getSeconds()));
    long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartTimestamp().getNanos());
    String elapsedSecondsStr =
        span.getEndTimestamp() != null
            ? String.format(
                "%13.6f",
                durationToNanos(span.getEndTimestamp().subtractTimestamp(span.getStartTimestamp()))
                    * 1.0e-9)
            : String.format("%13s", " ");

    SpanContext spanContext = span.getContext();
    formatter.format(
        "<b>%04d/%02d/%02d-%02d:%02d:%02d.%06d %s     TraceId: <b style=\"color:%s;\">%s</b> "
            + "SpanId: %s ParentSpanId: %s</b>%n",
        calendar.get(Calendar.YEAR),
        calendar.get(Calendar.MONTH) + 1,
        calendar.get(Calendar.DAY_OF_MONTH),
        calendar.get(Calendar.HOUR_OF_DAY),
        calendar.get(Calendar.MINUTE),
        calendar.get(Calendar.SECOND),
        microsField,
        elapsedSecondsStr,
        spanContext.getTraceOptions().isSampled()
            ? SAMPLED_TRACE_ID_COLOR
            : NOT_SAMPLED_TRACE_ID_COLOR,
        BaseEncoding.base16().lowerCase().encode(spanContext.getTraceId().getBytes()),
        BaseEncoding.base16().lowerCase().encode(spanContext.getSpanId().getBytes()),
        BaseEncoding.base16()
            .lowerCase()
            .encode(
                span.getParentSpanId() == null
                    ? SpanId.INVALID.getBytes()
                    : span.getParentSpanId().getBytes()));

    int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);

    Timestamp lastTimestampNanos = span.getStartTimestamp();
    TimedEvents<Annotation> annotations = span.getAnnotations();
    TimedEvents<io.opencensus.trace.NetworkEvent> networkEvents = span.getNetworkEvents();
    List<TimedEvent<?>> timedEvents = new ArrayList<TimedEvent<?>>(annotations.getEvents());
    timedEvents.addAll(networkEvents.getEvents());
    Collections.sort(timedEvents, new TimedEventComparator());
    for (TimedEvent<?> event : timedEvents) {
      // Special printing so that durations smaller than one second
      // are left padded with blanks instead of '0' characters.
      // E.g.,
      //        Number                  Printout
      //        ---------------------------------
      //        0.000534                  .   534
      //        1.000534                 1.000534
      long deltaMicros =
          TimeUnit.NANOSECONDS.toMicros(
              durationToNanos(event.getTimestamp().subtractTimestamp(lastTimestampNanos)));
      String deltaString;
      if (deltaMicros >= 1000000) {
        deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
      } else {
        deltaString = String.format(".%6d", deltaMicros);
      }

      calendar.setTimeInMillis(
          TimeUnit.SECONDS.toMillis(event.getTimestamp().getSeconds())
              + TimeUnit.NANOSECONDS.toMillis(event.getTimestamp().getNanos()));
      microsField = TimeUnit.NANOSECONDS.toMicros(event.getTimestamp().getNanos());

      int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
      if (dayOfYear == lastEntryDayOfYear) {
        formatter.format("%11s", "");
      } else {
        formatter.format(
            "%04d/%02d/%02d-",
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH) + 1,
            calendar.get(Calendar.DAY_OF_MONTH));
        lastEntryDayOfYear = dayOfYear;
      }

      formatter.format(
          "%02d:%02d:%02d.%06d %13s ... %s%n",
          calendar.get(Calendar.HOUR_OF_DAY),
          calendar.get(Calendar.MINUTE),
          calendar.get(Calendar.SECOND),
          microsField,
          deltaString,
          htmlEscaper()
              .escape(
                  event.getEvent() instanceof Annotation
                      ? renderAnnotation((Annotation) event.getEvent())
                      : renderNetworkEvents(
                          (io.opencensus.trace.NetworkEvent) castNonNull(event.getEvent()))));
      lastTimestampNanos = event.getTimestamp();
    }
    Status status = span.getStatus();
    if (status != null) {
      formatter.format("%44s %s%n", "", htmlEscaper().escape(renderStatus(status)));
    }
    formatter.format(
        "%44s %s%n",
        "", htmlEscaper().escape(renderAttributes(span.getAttributes().getAttributeMap())));
  }

  // TODO(sebright): Remove this method.
  @SuppressWarnings("nullness")
  private static <T> T castNonNull(@javax.annotation.Nullable T arg) {
    return arg;
  }

  // Emits the summary table with links to all samples.
  private void emitSummaryTable(PrintWriter out, Formatter formatter)
      throws UnsupportedEncodingException {
    if (runningSpanStore == null || sampledSpanStore == null) {
      return;
    }
    RunningSpanStore.Summary runningSpanStoreSummary = runningSpanStore.getSummary();
    SampledSpanStore.Summary sampledSpanStoreSummary = sampledSpanStore.getSummary();

    out.write("<table style='border-spacing: 0;\n");
    out.write("border-left:1px solid #3D3D3D;border-right:1px solid #3D3D3D;'>\n");
    emitSummaryTableHeader(out, formatter);

    Set<String> spanNames = new TreeSet<>(runningSpanStoreSummary.getPerSpanNameSummary().keySet());
    spanNames.addAll(sampledSpanStoreSummary.getPerSpanNameSummary().keySet());
    boolean zebraColor = true;
    for (String spanName : spanNames) {
      out.write("<tr class=\"border\">\n");
      if (!zebraColor) {
        out.write("<tr class=\"border\">\n");
      } else {
        formatter.format("<tr class=\"border\" style=\"background: %s\">%n", ZEBRA_STRIPE_COLOR);
      }
      zebraColor = !zebraColor;
      formatter.format("<td>%s</td>%n", htmlEscaper().escape(spanName));

      // Running
      out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
      RunningSpanStore.PerSpanNameSummary runningSpanStorePerSpanNameSummary =
          runningSpanStoreSummary.getPerSpanNameSummary().get(spanName);

      // subtype ignored for running requests.
      emitSingleCell(
          out,
          formatter,
          spanName,
          runningSpanStorePerSpanNameSummary == null
              ? 0
              : runningSpanStorePerSpanNameSummary.getNumRunningSpans(),
          RequestType.RUNNING,
          0);

      SampledSpanStore.PerSpanNameSummary sampledSpanStorePerSpanNameSummary =
          sampledSpanStoreSummary.getPerSpanNameSummary().get(spanName);

      // Latency based samples
      out.write("<td class=\"borderLC\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
      Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
          sampledSpanStorePerSpanNameSummary != null
              ? sampledSpanStorePerSpanNameSummary.getNumbersOfLatencySampledSpans()
              : null;
      int subtype = 0;
      for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
        if (latencyBucketsSummaries != null) {
          int numSamples =
              latencyBucketsSummaries.containsKey(latencyBucketsBoundaries)
                  ? latencyBucketsSummaries.get(latencyBucketsBoundaries)
                  : 0;
          emitSingleCell(out, formatter, spanName, numSamples, RequestType.FINISHED, subtype++);
        } else {
          // numSamples < -1 means "Not Available".
          emitSingleCell(out, formatter, spanName, -1, RequestType.FINISHED, subtype++);
        }
      }

      // Error based samples.
      out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
      if (sampledSpanStorePerSpanNameSummary != null) {
        Map<CanonicalCode, Integer> errorBucketsSummaries =
            sampledSpanStorePerSpanNameSummary.getNumbersOfErrorSampledSpans();
        int numErrorSamples = 0;
        for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
          numErrorSamples += it.getValue();
        }
        // subtype 0 means all;
        emitSingleCell(out, formatter, spanName, numErrorSamples, RequestType.FAILED, 0);
      } else {
        // numSamples < -1 means "Not Available".
        emitSingleCell(out, formatter, spanName, -1, RequestType.FAILED, 0);
      }

      out.write("</tr>\n");
    }
    out.write("</table>");
  }

  private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
    // First line.
    out.write("<tr class=\"bgcolor\">\n");
    out.write("<td colspan=1 class=\"head\"><b>Span Name</b></td>\n");
    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n");
    out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n");
    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    out.write("<td colspan=1 class=\"head\"><b>Error Samples</b></td>\n");
    out.write("</tr>\n");
    // Second line.
    out.write("<tr class=\"bgcolor\">\n");
    out.write("<td colspan=1></td>\n");
    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    out.write("<td colspan=1></td>\n");
    out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
      formatter.format(
          "<td colspan=1 class=\"centerW\"><b>[%s]</b></td>%n",
          LATENCY_BUCKET_BOUNDARIES_STRING_MAP.get(latencyBucketsBoundaries));
    }
    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
    out.write("<td colspan=1></td>\n");
    out.write("</tr>\n");
  }

  // If numSamples is greater than 0 then emit a link to see span data, if the numSamples is
  // negative then print "N/A", otherwise print the text "0".
  private static void emitSingleCell(
      PrintWriter out,
      Formatter formatter,
      String spanName,
      int numSamples,
      RequestType type,
      int subtype)
      throws UnsupportedEncodingException {
    if (numSamples > 0) {
      formatter.format(
          "<td class=\"center\"><a href='?%s=%s&%s=%d&%s=%d'>%d</a></td>%n",
          HEADER_SPAN_NAME,
          URLEncoder.encode(spanName, "UTF-8"),
          HEADER_SAMPLES_TYPE,
          type.getValue(),
          HEADER_SAMPLES_SUB_TYPE,
          subtype,
          numSamples);
    } else if (numSamples < 0) {
      out.write("<td class=\"center\">N/A</td>\n");
    } else {
      out.write("<td class=\"center\">0</td>\n");
    }
  }

  private static void emitLegend(PrintWriter out) {
    out.write("<br>\n");
    out.printf(
        "<p><b style=\"color:%s;\">TraceId</b> means sampled request. "
            + "<b style=\"color:%s;\">TraceId</b> means not sampled request.</p>%n",
        SAMPLED_TRACE_ID_COLOR, NOT_SAMPLED_TRACE_ID_COLOR);
  }

  private static Map<LatencyBucketBoundaries, String> buildLatencyBucketBoundariesStringMap() {
    Map<LatencyBucketBoundaries, String> ret = new HashMap<>();
    for (LatencyBucketBoundaries latencyBucketBoundaries : LatencyBucketBoundaries.values()) {
      ret.put(latencyBucketBoundaries, latencyBucketBoundariesToString(latencyBucketBoundaries));
    }
    return Collections.unmodifiableMap(ret);
  }

  private static long durationToNanos(Duration duration) {
    return TimeUnit.SECONDS.toNanos(duration.getSeconds()) + duration.getNanos();
  }

  private static String latencyBucketBoundariesToString(
      LatencyBucketBoundaries latencyBucketBoundaries) {
    switch (latencyBucketBoundaries) {
      case ZERO_MICROSx10:
        return ">0us";
      case MICROSx10_MICROSx100:
        return ">10us";
      case MICROSx100_MILLIx1:
        return ">100us";
      case MILLIx1_MILLIx10:
        return ">1ms";
      case MILLIx10_MILLIx100:
        return ">10ms";
      case MILLIx100_SECONDx1:
        return ">100ms";
      case SECONDx1_SECONDx10:
        return ">1s";
      case SECONDx10_SECONDx100:
        return ">10s";
      case SECONDx100_MAX:
        return ">100s";
    }
    throw new IllegalArgumentException("No value string available for: " + latencyBucketBoundaries);
  }

  @SuppressWarnings("deprecation")
  private static String renderNetworkEvents(io.opencensus.trace.NetworkEvent networkEvent) {
    StringBuilder stringBuilder = new StringBuilder();
    if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV) {
      stringBuilder.append("Received message");
    } else if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.SENT) {
      stringBuilder.append("Sent message");
    } else {
      stringBuilder.append("Unknown");
    }
    stringBuilder.append(" id=");
    stringBuilder.append(networkEvent.getMessageId());
    stringBuilder.append(" uncompressed_size=");
    stringBuilder.append(networkEvent.getUncompressedMessageSize());
    stringBuilder.append(" compressed_size=");
    stringBuilder.append(networkEvent.getCompressedMessageSize());
    return stringBuilder.toString();
  }

  private static String renderAnnotation(Annotation annotation) {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(annotation.getDescription());
    if (!annotation.getAttributes().isEmpty()) {
      stringBuilder.append(" ");
      stringBuilder.append(renderAttributes(annotation.getAttributes()));
    }
    return stringBuilder.toString();
  }

  private static String renderStatus(Status status) {
    return status.toString();
  }

  private static String renderAttributes(Map<String, AttributeValue> attributes) {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("Attributes:{");
    boolean first = true;
    for (Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
      if (first) {
        first = false;
        stringBuilder.append(entry.getKey());
        stringBuilder.append("=");
        stringBuilder.append(attributeValueToString(entry.getValue()));
      } else {
        stringBuilder.append(", ");
        stringBuilder.append(entry.getKey());
        stringBuilder.append("=");
        stringBuilder.append(attributeValueToString(entry.getValue()));
      }
    }
    stringBuilder.append("}");
    return stringBuilder.toString();
  }

  // The return type needs to be nullable when this function is used as an argument to 'match' in
  // attributeValueToString, because 'match' doesn't allow covariant return types.
  private static final Function<Object, /*@Nullable*/ String> returnToString =
      Functions.returnToString();

  @javax.annotation.Nullable
  private static String attributeValueToString(AttributeValue attributeValue) {
    return attributeValue.match(
        returnToString,
        returnToString,
        returnToString,
        returnToString,
        Functions.</*@Nullable*/ String>returnNull());
  }

  private static final class TimedEventComparator
      implements Comparator<TimedEvent<?>>, Serializable {
    private static final long serialVersionUID = 0;

    @Override
    public int compare(TimedEvent<?> o1, TimedEvent<?> o2) {
      return o1.getTimestamp().compareTo(o2.getTimestamp());
    }
  }

  private static final class SpanDataComparator implements Comparator<SpanData>, Serializable {
    private static final long serialVersionUID = 0;
    private final boolean incremental;

    /**
     * Returns a new {@code SpanDataComparator}.
     *
     * @param incremental {@code true} if sorted incremental.
     */
    private SpanDataComparator(boolean incremental) {
      this.incremental = incremental;
    }

    @Override
    public int compare(SpanData o1, SpanData o2) {
      return incremental
          ? o1.getStartTimestamp().compareTo(o2.getStartTimestamp())
          : o2.getStartTimestamp().compareTo(o1.getStartTimestamp());
    }
  }
}
