/*
 * Copyright 2016-present Facebook, Inc.
 *
 * 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.facebook.buck.util.trace;

import com.facebook.buck.io.ProjectFilesystem;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Event-driven parser for
 * <a href="https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview">
 * Chrome traces</a>.
 */
public class ChromeTraceParser {

  /**
   * Extracts data of interest if it finds a Chrome trace event of the type it is looking for.
   */
  public interface ChromeTraceEventMatcher<T> {
    /**
     * Tests the specified event to see if it is a match. Its name has already been extracted, for
     * convenience. If it is a match, it should return the data of interest in the return value. If
     * not, then it should return {@link Optional#empty()}.
     */
    Optional<T> test(JsonObject event, String name);
  }

  /**
   * Tries to extract the command that was used to trigger the invocation of Buck that generated the
   * trace. If found, it returns the command as an opaque string.
   */
  public static final ChromeTraceEventMatcher<String> COMMAND = (json, name) -> {
    JsonElement argsEl = json.get("args");
    if (argsEl == null ||
        !argsEl.isJsonObject() ||
        argsEl.getAsJsonObject().get("command_args") == null ||
        !argsEl.getAsJsonObject().get("command_args").isJsonPrimitive()) {
      return Optional.empty();
    }

    String commandArgs = argsEl.getAsJsonObject().get("command_args").getAsString();
    String command = "buck " + name + " " + commandArgs;

    return Optional.of(command);
  };

  private final ProjectFilesystem projectFilesystem;

  public ChromeTraceParser(ProjectFilesystem projectFilesystem) {
    this.projectFilesystem = projectFilesystem;
  }

  /**
   * Parses a Chrome trace and stops parsing once all of the specified matchers have been
   * satisfied. This method parses only one Chrome trace event at a time, which avoids loading the
   * entire trace into memory.
   * @param pathToTrace is a relative path [to the ProjectFilesystem] to a Chrome trace in the
   *     "JSON Array Format."
   * @param chromeTraceEventMatchers set of matchers this invocation of {@code parse()} is trying to
   *     satisfy. Once a matcher finds a match, it will not consider any other events in the trace.
   * @return a {@code Map} where every matcher that found a match will have an entry whose key is
   *     the matcher and whose value is the one returned by
   *     {@link ChromeTraceEventMatcher#test(JsonObject, String)} without the {@link Optional}
   *     wrapper.
   */
  public Map<ChromeTraceEventMatcher<?>, Object> parse(
      Path pathToTrace,
      Set<ChromeTraceEventMatcher<?>> chromeTraceEventMatchers) throws IOException {
    Set<ChromeTraceEventMatcher<?>> unmatchedMatchers = new HashSet<>(chromeTraceEventMatchers);
    Preconditions.checkArgument(!unmatchedMatchers.isEmpty(), "Must specify at least one matcher");
    Map<ChromeTraceEventMatcher<?>, Object> results = new HashMap<>();

    try (
        InputStream input = projectFilesystem.newFileInputStream(pathToTrace);
        JsonReader jsonReader = new JsonReader(new InputStreamReader(input))) {
      jsonReader.beginArray();
      Gson gson = new Gson();

      featureSearch:
      while (true) {
        // If END_ARRAY is the next token, then there are no more elements in the array.
        if (jsonReader.peek().equals(JsonToken.END_ARRAY)) {
          break;
        }

        // Verify and extract the name property before invoking any of the matchers.
        JsonElement eventEl = gson.fromJson(jsonReader, JsonElement.class);
        JsonObject event = eventEl.getAsJsonObject();
        JsonElement nameEl = event.get("name");
        if (nameEl == null || !nameEl.isJsonPrimitive()) {
          continue;
        }
        String name = nameEl.getAsString();

        // Prefer Iterator to Iterable+foreach so we can use remove().
        for (Iterator<ChromeTraceEventMatcher<?>> iter = unmatchedMatchers.iterator();
             iter.hasNext(); ) {
          ChromeTraceEventMatcher<?> chromeTraceEventMatcher = iter.next();
          Optional<?> result = chromeTraceEventMatcher.test(event, name);
          if (result.isPresent()) {
            iter.remove();
            results.put(chromeTraceEventMatcher, result.get());

            if (unmatchedMatchers.isEmpty()) {
              break featureSearch;
            }
          }
        }
      }
    }

    // We could throw if !unmatchedMatchers.isEmpty(), but that might be overbearing.
    return results;
  }

  /**
   * Designed for use with the result of {@link ChromeTraceParser#parse(Path, Set)}.
   * Helper function to avoid some distasteful casting logic.
   */
  @SuppressWarnings("unchecked")
  public static <T> Optional<T> getResultForMatcher(
      ChromeTraceEventMatcher<T> matcher,
      Map<ChromeTraceEventMatcher<?>, Object> results) {
    T result = (T) results.get(matcher);
    return Optional.ofNullable(result);
  }
}

@@ -2527,16 +2527,46 @@
 + name +
+ (commandArgs.isEmpty() ? %22%22 :
  %22 %22 + c
@@ -2575,16 +2575,17 @@
 mandArgs
+)
 ;%0A%0A    r

Remove an extra space added when subcommand has no arguments in ChromeTraceParser.