// 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.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.query2.NamedThreadSafeOutputFormatterCallback;
import com.google.devtools.build.lib.query2.PostAnalysisQueryEnvironment;
import com.google.devtools.build.lib.query2.PostAnalysisQueryEnvironment.TopLevelConfigurations;
import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
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.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.Query;
import com.google.devtools.build.lib.server.FailureDetails.Query.Code;
import com.google.devtools.build.lib.skyframe.SkyframeExecutorWrappingWalkableGraph;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.WalkableGraph;
import java.io.IOException;
import java.util.Collection;

/**
 * Version of {@link BuildTool} that handles all work for queries based on results from the analysis
 * phase.
 */
public abstract class PostAnalysisQueryBuildTool<T> extends BuildTool {

  private final QueryExpression queryExpression;

  public PostAnalysisQueryBuildTool(CommandEnvironment env, QueryExpression queryExpression) {
    super(env);
    this.queryExpression = queryExpression;
  }

  @Override
  protected void postProcessAnalysisResult(BuildRequest request, AnalysisResult analysisResult)
      throws InterruptedException, ViewCreationFailedException, ExitException {
    // TODO: b/71905538 - this query will operate over the graph as constructed by analysis, but
    // will also pick up any nodes that are in the graph from prior builds. This makes the results
    // not reproducible at the level of a single command. Either tolerate, or wipe the analysis
    // graph beforehand if this option is specified, or add another option to wipe if desired
    // (SkyframeExecutor#handleAnalysisInvalidatingChange should be sufficient).
    if (queryExpression != null) {
      if (!env.getSkyframeExecutor().tracksStateForIncrementality()) {
        throw new ExitException(
            DetailedExitCode.of(
                FailureDetail.newBuilder()
                    .setMessage(
                        "Queries based on analysis results are not allowed if incrementality state"
                            + " is not being kept")
                    .setQuery(Query.newBuilder().setCode(Code.ANALYSIS_QUERY_PREREQ_UNMET))
                    .build()));
      }
      try (QueryRuntimeHelper queryRuntimeHelper =
          env.getRuntime().getQueryRuntimeHelperFactory().create(env)) {
        doPostAnalysisQuery(
            request,
            analysisResult.getConfigurationCollection().getHostConfiguration(),
            new TopLevelConfigurations(analysisResult.getTopLevelTargetsWithConfigs()),
            env.getSkyframeExecutor().getTransitiveConfigurationKeys(),
            queryRuntimeHelper,
            queryExpression);
      } catch (QueryException | IOException e) {
        if (!request.getKeepGoing()) {
          throw new ViewCreationFailedException("Error doing post analysis query", e);
        }
        env.getReporter().error(null, "Error doing post analysis query", e);
      } catch (QueryRuntimeHelperException e) {
        throw new ExitException(DetailedExitCode.of(e.getFailureDetail()));
      }
    }
  }

  protected abstract PostAnalysisQueryEnvironment<T> getQueryEnvironment(
      BuildRequest request,
      BuildConfiguration hostConfiguration,
      TopLevelConfigurations topLevelConfigurations,
      Collection<SkyKey> transitiveConfigurationKeys,
      WalkableGraph walkableGraph)
      throws InterruptedException;

  private void doPostAnalysisQuery(
      BuildRequest request,
      BuildConfiguration hostConfiguration,
      TopLevelConfigurations topLevelConfigurations,
      Collection<SkyKey> transitiveConfigurationKeys,
      QueryRuntimeHelper queryRuntimeHelper,
      QueryExpression queryExpression)
      throws InterruptedException, QueryException, IOException, QueryRuntimeHelperException {
    WalkableGraph walkableGraph =
        SkyframeExecutorWrappingWalkableGraph.of(env.getSkyframeExecutor());

    PostAnalysisQueryEnvironment<T> postAnalysisQueryEnvironment =
        getQueryEnvironment(
            request,
            hostConfiguration,
            topLevelConfigurations,
            transitiveConfigurationKeys,
            walkableGraph);

    Iterable<NamedThreadSafeOutputFormatterCallback<T>> callbacks =
        postAnalysisQueryEnvironment.getDefaultOutputFormatters(
            postAnalysisQueryEnvironment.getAccessor(),
            env.getReporter(),
            queryRuntimeHelper.getOutputStreamForQueryOutput(),
            env.getSkyframeExecutor(),
            hostConfiguration,
            runtime.getRuleClassProvider().getTrimmingTransitionFactory(),
            env.getPackageManager());
    String outputFormat = postAnalysisQueryEnvironment.getOutputFormat();
    NamedThreadSafeOutputFormatterCallback<T> callback =
        NamedThreadSafeOutputFormatterCallback.selectCallback(outputFormat, callbacks);
    if (callback == null) {
      env.getReporter()
          .handle(
              Event.error(
                  String.format(
                      "Invalid output format '%s'. Valid values are: %s",
                      outputFormat,
                      NamedThreadSafeOutputFormatterCallback.callbackNames(callbacks))));
      return;
    }
    QueryEvalResult result =
        postAnalysisQueryEnvironment.evaluateQuery(queryExpression, callback);
    if (result.isEmpty()) {
      env.getReporter().handle(Event.info("Empty query results"));
    }
    queryRuntimeHelper.afterQueryOutputIsWritten();
  }
}
