// Copyright 2018 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.buildtool;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.analysis.AnalysisProtos.ActionGraphContainer;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.query2.PostAnalysisQueryEnvironment;
import com.google.devtools.build.lib.query2.PostAnalysisQueryEnvironment.TopLevelConfigurations;
import com.google.devtools.build.lib.query2.aquery.ActionGraphProtoV2OutputFormatterCallback;
import com.google.devtools.build.lib.query2.aquery.ActionGraphQueryEnvironment;
import com.google.devtools.build.lib.query2.aquery.AqueryActionFilter;
import com.google.devtools.build.lib.query2.aquery.AqueryOptions;
import com.google.devtools.build.lib.query2.engine.ActionFilterFunction;
import com.google.devtools.build.lib.query2.engine.FunctionExpression;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.QueryRuntimeHelper;
import com.google.devtools.build.lib.runtime.QueryRuntimeHelper.QueryRuntimeHelperException;
import com.google.devtools.build.lib.server.FailureDetails.ActionQuery;
import com.google.devtools.build.lib.server.FailureDetails.ActionQuery.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetValue;
import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.ActionGraphDump;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler.OutputType;
import com.google.devtools.build.lib.skyframe.actiongraph.v2.InvalidAqueryOutputFormatException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.WalkableGraph;
import com.google.protobuf.TextFormat;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;

/** A version of {@link BuildTool} that handles all aquery work. */
public final class AqueryBuildTool extends PostAnalysisQueryBuildTool<ConfiguredTargetValue> {
  private final AqueryActionFilter actionFilters;

  public AqueryBuildTool(CommandEnvironment env, @Nullable QueryExpression queryExpression)
      throws AqueryActionFilterException {
    super(env, queryExpression);
    actionFilters = buildActionFilters(queryExpression);
  }

  /** Outputs the current {@link ActionGraphContainer} of Skyframe. */
  public BlazeCommandResult dumpActionGraphFromSkyframe(BuildRequest request) {
    try (QueryRuntimeHelper queryRuntimeHelper =
        env.getRuntime().getQueryRuntimeHelperFactory().create(env)) {
      AqueryOptions aqueryOptions = request.getOptions(AqueryOptions.class);

      PrintStream printStream =
          queryRuntimeHelper.getOutputStreamForQueryOutput() == null
              ? null
              : new PrintStream(queryRuntimeHelper.getOutputStreamForQueryOutput());

      if (aqueryOptions.protoV2) {
        try (AqueryOutputHandler aqueryOutputHandler =
            ActionGraphProtoV2OutputFormatterCallback.constructAqueryOutputHandler(
                OutputType.fromString(aqueryOptions.outputFormat),
                queryRuntimeHelper.getOutputStreamForQueryOutput(),
                printStream)) {
          ActionGraphDump actionGraphDump =
              new ActionGraphDump(
                  aqueryOptions.includeCommandline,
                  aqueryOptions.includeArtifacts,
                  actionFilters,
                  aqueryOptions.includeParamFiles,
                  aqueryOutputHandler);
          ((SequencedSkyframeExecutor) env.getSkyframeExecutor())
              .dumpSkyframeState(actionGraphDump);
        } catch (InvalidAqueryOutputFormatException e) {
          String message =
              "--skyframe_state must be used with --output=proto|textproto|jsonproto. "
                  + e.getMessage();
          env.getReporter().handle(Event.error(message));
          return getFailureResult(message, Code.SKYFRAME_STATE_PREREQ_UNMET);
        }
      } else {
        // TODO(b/154500246) Remove this code path.
        ActionGraphContainer actionGraphContainer =
            ((SequencedSkyframeExecutor) env.getSkyframeExecutor())
                .getActionGraphContainer(
                    aqueryOptions.includeCommandline,
                    actionFilters,
                    aqueryOptions.includeParamFiles,
                    aqueryOptions.includeArtifacts);

        // Write the data.
        if (OutputType.BINARY.formatName().equals(aqueryOptions.outputFormat)) {
          actionGraphContainer.writeTo(printStream);
        } else if (OutputType.TEXT.formatName().equals(aqueryOptions.outputFormat)) {
          TextFormat.printer().print(actionGraphContainer, printStream);
        } else {
          String message =
              String.format(
                  "Unsupported output format %s: --skyframe_state must be used with --output=proto"
                      + " or --output=textproto.",
                  aqueryOptions.outputFormat);
          env.getReporter().handle(Event.error(message));
          return getFailureResult(message, Code.SKYFRAME_STATE_PREREQ_UNMET);
        }
      }
      return BlazeCommandResult.success();
    } catch (CommandLineExpansionException e) {
      String message = "Error while parsing command: " + e.getMessage();
      env.getReporter().handle(Event.error(message));
      return getFailureResult(message, Code.COMMAND_LINE_EXPANSION_FAILURE);
    } catch (IOException e) {
      String message = "Error while emitting output: " + e.getMessage();
      env.getReporter().handle(Event.error(message));
      return getFailureResult(message, Code.OUTPUT_FAILURE);
    } catch (QueryRuntimeHelperException e) {
      env.getReporter().handle(Event.error(e.getMessage()));
      return BlazeCommandResult.failureDetail(e.getFailureDetail());
    }
  }

  @Override
  protected PostAnalysisQueryEnvironment<ConfiguredTargetValue> getQueryEnvironment(
      BuildRequest request,
      BuildConfiguration hostConfiguration,
      TopLevelConfigurations topLevelConfigurations,
      Collection<SkyKey> transitiveConfigurationKeys,
      WalkableGraph walkableGraph) {
    ImmutableList<QueryFunction> extraFunctions =
        new ImmutableList.Builder<QueryFunction>()
            .addAll(ActionGraphQueryEnvironment.AQUERY_FUNCTIONS)
            .addAll(env.getRuntime().getQueryFunctions())
            .build();
    AqueryOptions aqueryOptions = request.getOptions(AqueryOptions.class);

    ActionGraphQueryEnvironment queryEnvironment =
        new ActionGraphQueryEnvironment(
            request.getKeepGoing(),
            env.getReporter(),
            extraFunctions,
            topLevelConfigurations,
            hostConfiguration,
            env.getRelativeWorkingDirectory().getPathString(),
            env.getPackageManager().getPackagePath(),
            () -> walkableGraph,
            aqueryOptions);
    queryEnvironment.setActionFilters(actionFilters);

    return queryEnvironment;
  }

  /**
   * Return the action filters in the form { inputs: <pattern>, outputs: <pattern>, ... }
   *
   * @param queryExpression The query expression from aquery command
   * @return the action filters
   * @throws AqueryActionFilterException if an aquery filter function is preceded by any other
   *     function types
   */
  private AqueryActionFilter buildActionFilters(@Nullable QueryExpression queryExpression)
      throws AqueryActionFilterException {
    AqueryActionFilter.Builder actionFiltersBuilder = AqueryActionFilter.builder();

    if (!(queryExpression instanceof FunctionExpression)) {
      return actionFiltersBuilder.build();
    }

    Optional<FunctionExpression> functionExpressionOptional =
        Optional.of((FunctionExpression) queryExpression);

    FunctionExpression nonAqueryFilterFunctionExpression = null;

    // Unwrap the function layers
    // Validate that aquery filter functions (inputs, outputs, mnemonics) are not preceded
    // by any other function types
    while (functionExpressionOptional.isPresent()) {
      FunctionExpression functionExpression = functionExpressionOptional.get();

      if (functionExpression.getFunction() instanceof ActionFilterFunction) {
        if (nonAqueryFilterFunctionExpression != null) {
          throw new AqueryActionFilterException(
              "aquery filter functions (inputs, outputs, mnemonic) produce actions, and therefore "
                  + "can't be the input of other function types: "
                  + nonAqueryFilterFunctionExpression.getFunction().getName());
        }
        ActionFilterFunction actionFilterFunction =
            (ActionFilterFunction) functionExpression.getFunction();

        String patternString = functionExpression.getArgs().get(0).getWord();
        try {
          actionFiltersBuilder.put(actionFilterFunction.getName(), Pattern.compile(patternString));
        } catch (PatternSyntaxException e) {
          throw new AqueryActionFilterException("Wrong query syntax: " + e.getMessage());
        }
      } else {
        nonAqueryFilterFunctionExpression = functionExpression;
      }

      functionExpressionOptional = getNextFunctionExpression(functionExpression);
    }

    return actionFiltersBuilder.build();
  }

  /**
   * Unwrap input {@code functionExpression} to get the next FunctionExpression in the query
   *
   * @param functionExpression the current function expression
   * @return the Optional of the next FunctionExpression in the query
   */
  private Optional<FunctionExpression> getNextFunctionExpression(
      FunctionExpression functionExpression) {
    for (Argument arg : functionExpression.getArgs()) {
      if (arg.getType() == ArgumentType.EXPRESSION
          && arg.getExpression() instanceof FunctionExpression) {
        return Optional.of((FunctionExpression) arg.getExpression());
      }
    }
    return Optional.empty();
  }

  /** Custom exception class for aquery filtering */
  public static class AqueryActionFilterException extends Exception {
    AqueryActionFilterException(String message) {
      super(message);
    }
  }

  private static BlazeCommandResult getFailureResult(String message, Code detailedCode) {
    return BlazeCommandResult.failureDetail(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setActionQuery(ActionQuery.newBuilder().setCode(detailedCode))
            .build());
  }
}
