// Copyright 2020 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.profiler;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Represents a single trace event in a JSON profile.
 *
 * <p>Format is documented in
 * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
 */
@AutoValue
public abstract class TraceEvent {
  public static TraceEvent create(
      @Nullable String category,
      String name,
      @Nullable String type,
      @Nullable Duration timestamp,
      @Nullable Duration duration,
      long threadId,
      @Nullable ImmutableMap<String, Object> args,
      @Nullable String primaryOutputPath,
      @Nullable String targetLabel,
      @Nullable String mnemonic) {
    return new AutoValue_TraceEvent(
        category,
        name,
        type,
        timestamp,
        duration,
        threadId,
        args,
        primaryOutputPath,
        targetLabel,
        mnemonic);
  }

  @Nullable
  public abstract String category();

  public abstract String name();

  @Nullable
  public abstract String type();

  @Nullable
  public abstract Duration timestamp();

  @Nullable
  public abstract Duration duration();

  public abstract long threadId();

  @Nullable
  public abstract ImmutableMap<String, Object> args();

  // Only applicable to action-related TraceEvents.
  @Nullable
  public abstract String primaryOutputPath();

  @Nullable
  public abstract String targetLabel();

  @Nullable
  public abstract String mnemonic();

  private static TraceEvent createFromJsonReader(JsonReader reader) throws IOException {
    String category = null;
    String name = null;
    Duration timestamp = null;
    Duration duration = null;
    long threadId = -1;
    String primaryOutputPath = null;
    String targetLabel = null;
    String mnemonic = null;
    String type = null;
    ImmutableMap<String, Object> args = null;

    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.nextName()) {
        case "cat":
          category = reader.nextString();
          break;
        case "name":
          name = reader.nextString();
          break;
        case "ph":
          type = reader.nextString();
          break;
        case "ts":
          // Duration has no microseconds :-/.
          timestamp = Duration.ofNanos(reader.nextLong() * 1000);
          break;
        case "dur":
          duration = Duration.ofNanos(reader.nextLong() * 1000);
          break;
        case "tid":
          threadId = reader.nextLong();
          break;
        case "out":
          primaryOutputPath = reader.nextString();
          break;
        case "args":
          args = parseMap(reader);
          Object target = args.get("target");
          targetLabel = target instanceof String ? (String) target : null;
          Object mnemonicValue = args.get("mnemonic");
          mnemonic = mnemonicValue instanceof String ? (String) mnemonicValue : null;
          break;
        default:
          reader.skipValue();
      }
    }
    reader.endObject();
    return TraceEvent.create(
        category,
        name,
        type,
        timestamp,
        duration,
        threadId,
        args,
        primaryOutputPath,
        targetLabel,
        mnemonic);
  }

  private static ImmutableMap<String, Object> parseMap(JsonReader reader) throws IOException {
    ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();

    reader.beginObject();
    while (reader.peek() != JsonToken.END_OBJECT) {
      String name = reader.nextName();
      Object val = parseSingleValueRecursively(reader);
      builder.put(name, val);
    }
    reader.endObject();

    return builder.buildOrThrow();
  }

  private static ImmutableList<Object> parseArray(JsonReader reader) throws IOException {
    ImmutableList.Builder<Object> builder = ImmutableList.builder();

    reader.beginArray();
    while (reader.peek() != JsonToken.END_ARRAY) {
      Object val = parseSingleValueRecursively(reader);
      builder.add(val);
    }
    reader.endArray();

    return builder.build();
  }

  @Nullable
  private static Object parseSingleValueRecursively(JsonReader reader) throws IOException {
    JsonToken nextToken = reader.peek();
    switch (nextToken) {
      case BOOLEAN:
        return reader.nextBoolean();
      case NULL:
        reader.nextNull();
        return null;
      case NUMBER:
        // Json's only numeric type is number, using Double to accommodate all types
        return reader.nextDouble();
      case STRING:
        return reader.nextString();
      case BEGIN_OBJECT:
        return parseMap(reader);
      case BEGIN_ARRAY:
        return parseArray(reader);
      default:
        throw new IOException("Unexpected token " + nextToken.name());
    }
  }

  public static List<TraceEvent> parseTraceEvents(JsonReader reader) throws IOException {
    List<TraceEvent> traceEvents = new ArrayList<>();
    reader.beginArray();
    while (reader.hasNext()) {
      traceEvents.add(createFromJsonReader(reader));
    }
    reader.endArray();
    return traceEvents;
  }
}
