// Copyright 2014 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.runtime.commands;

import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.profiler.JsonProfile;
import com.google.devtools.build.lib.profiler.output.PhaseText;
import com.google.devtools.build.lib.profiler.statistics.CriticalPathStatistics;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.ProfileCommand.Code;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;

/** Command line wrapper for analyzing Blaze build profiles. */
@Command(
  name = "analyze-profile",
  options = {ProfileCommand.ProfileOptions.class},
  shortDescription = "Analyzes build profile data.",
  help = "resource:analyze-profile.txt",
  allowResidue = true,
  completion = "path",
  mustRunInWorkspace = false
)
public final class ProfileCommand implements BlazeCommand {

  public static class DumpConverter extends Converters.StringSetConverter {
    public DumpConverter() {
      super("text", "raw");
    }
  }

  public static class ProfileOptions extends OptionsBase {
    @Option(
        name = "dump",
        abbrev = 'd',
        converter = DumpConverter.class,
        defaultValue = "null",
        documentationCategory = OptionDocumentationCategory.LOGGING,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        help =
            "output full profile data dump either in human-readable 'text' format or"
                + " script-friendly 'raw' format.")
    public String dumpMode;
  }

  /**
   * Note that this is just a basic check whether the file is zlib compressed.
   *
   * <p>Other checks (e.g. the magic bytes of the binary profile file) are done later.
   */
  private static boolean isOldBinaryProfile(File profile) {
    try (InputStream inputStream = new FileInputStream(profile)) {
      byte[] magicBytes = new byte[2];
      int numBytesRead = inputStream.read(magicBytes);
      if (numBytesRead == 2
          && magicBytes[0] == (byte) 0x78
          && (magicBytes[1] == (byte) 0x01
              || magicBytes[1] == (byte) 0x9C
              || magicBytes[1] == (byte) 0xDA)) {
        return true;
      }
    } catch (Exception e) {
      // silently ignore any exception
    }
    return false;
  }

  @Override
  public BlazeCommandResult exec(final CommandEnvironment env, OptionsParsingResult options) {
    ProfileOptions profileOptions = options.getOptions(ProfileOptions.class);
    String dumpMode = profileOptions.dumpMode;

    try (PrintStream out = getOutputStream(env)) {
      Reporter reporter = env.getReporter();

      reporter.handle(
          Event.warn(
              "This information is intended for consumption by Bazel developers"
                  + " only, and may change at any time. Script against it at your own risk"));
      for (String name : options.getResidue()) {
        Path profileFile = env.getWorkingDirectory().getRelative(name);
        if (isOldBinaryProfile(profileFile.getPathFile())) {
          String message =
              "The old binary profile format is deprecated."
                  + " Use the JSON trace profile instead.";
          reporter.handle(Event.error(message));
          return createFailureResult(message, Code.OLD_BINARY_FORMAT_UNSUPPORTED);
        } else {
          try {
            if (dumpMode != null) {
              reporter.handle(
                  Event.warn(
                      "--dump has not been implemented yet for the JSON profile, ignoring."));
            }
            JsonProfile jsonProfile = new JsonProfile(profileFile.getPathFile());

            JsonProfile.BuildMetadata buildMetadata = jsonProfile.getBuildMetadata();
            if (buildMetadata != null) {
              reporter.handle(
                  Event.info(
                      "Profile created on "
                          + buildMetadata.date()
                          + ", build ID: "
                          + buildMetadata.buildId()
                          + ", output base: "
                          + buildMetadata.outputBase()));
            }

            new PhaseText(
                    out,
                    jsonProfile.getPhaseSummaryStatistics(),
                    new CriticalPathStatistics(jsonProfile.getTraceEvents()))
                .print();
          } catch (IOException e) {
            String message = "Failed to analyze profile file(s): " + e.getMessage();
            reporter.handle(Event.error(message));
            return createFailureResult(message, Code.FILE_READ_FAILURE);
          }
        }
      }
    }
    return BlazeCommandResult.success();
  }

  private static PrintStream getOutputStream(CommandEnvironment env) {
    return new PrintStream(
        new BufferedOutputStream(env.getReporter().getOutErr().getOutputStream()), false);
  }

  private static BlazeCommandResult createFailureResult(String message, Code detailedCode) {
    return BlazeCommandResult.failureDetail(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setProfileCommand(FailureDetails.ProfileCommand.newBuilder().setCode(detailedCode))
            .build());
  }
}
