/*
 * Copyright 2018-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.rules.modern.builders;

import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.LeafEvents;
import com.facebook.buck.rules.modern.builders.FileTreeBuilder.InputFile;
import com.facebook.buck.rules.modern.builders.FileTreeBuilder.ProtocolTreeBuilder;
import com.facebook.buck.rules.modern.builders.Protocol.Digest;
import com.facebook.buck.rules.modern.builders.Protocol.Directory;
import com.facebook.buck.rules.modern.builders.Protocol.OutputDirectory;
import com.facebook.buck.rules.modern.builders.Protocol.OutputFile;
import com.facebook.buck.rules.modern.builders.Protocol.Tree;
import com.facebook.buck.util.Ansi;
import com.facebook.buck.util.CapturingPrintStream;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.DefaultProcessExecutor;
import com.facebook.buck.util.ProcessExecutor.Result;
import com.facebook.buck.util.ProcessExecutorParams;
import com.facebook.buck.util.ProcessExecutorParams.Builder;
import com.facebook.buck.util.RichStream;
import com.facebook.buck.util.Scope;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.util.function.ThrowingSupplier;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.MoreFiles;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
 * Runs an action (command + environment) in a directory and returns the results (exit code,
 * stdout/stderr, and outputs).
 */
public class ActionRunner {
  private final Protocol protocol;
  private final BuckEventBus eventBus;

  public ActionRunner(Protocol protocol, BuckEventBus eventBus) {
    this.protocol = protocol;
    this.eventBus = eventBus;
  }

  /** Results of an action. */
  public static class ActionResult {
    public final ImmutableList<OutputFile> outputFiles;
    public final ImmutableList<OutputDirectory> outputDirectories;
    public final ImmutableMap<Protocol.Digest, ThrowingSupplier<InputStream, IOException>>
        requiredData;
    public final int exitCode;
    public final String stderr;
    public final String stdout;

    ActionResult(
        ImmutableList<OutputFile> outputFiles,
        ImmutableList<OutputDirectory> outputDirectories,
        ImmutableMap<Digest, ThrowingSupplier<InputStream, IOException>> requiredData,
        int exitCode,
        String stderr,
        String stdout) {
      this.outputFiles = outputFiles;
      this.outputDirectories = outputDirectories;
      this.requiredData = requiredData;
      this.exitCode = exitCode;
      this.stderr = stderr;
      this.stdout = stdout;
    }
  }

  /** Runs an action and returns the result. */
  public ActionResult runAction(
      ImmutableList<String> command,
      ImmutableMap<String, String> environment,
      Set<Path> outputs,
      Path buildDir)
      throws IOException, InterruptedException {
    Console console;
    Builder paramsBuilder;
    try (Scope ignored = LeafEvents.scope(eventBus, "preparing_action")) {
      paramsBuilder = ProcessExecutorParams.builder();
      paramsBuilder.setCommand(command);
      paramsBuilder.setEnvironment(environment);
      paramsBuilder.setDirectory(buildDir);
      CapturingPrintStream stdOut = new CapturingPrintStream();
      CapturingPrintStream stdErr = new CapturingPrintStream();
      console = new Console(Verbosity.STANDARD_INFORMATION, stdOut, stdErr, Ansi.withoutTty());
    }

    Result result;
    try (Scope ignored = LeafEvents.scope(eventBus, "subprocess")) {
      result = new DefaultProcessExecutor(console).launchAndExecute(paramsBuilder.build());
    }

    ImmutableList.Builder<OutputFile> outputFiles;
    ImmutableList.Builder<OutputDirectory> outputDirectories;
    ImmutableMap.Builder<Digest, ThrowingSupplier<InputStream, IOException>> requiredData;
    try (Scope ignored = LeafEvents.scope(eventBus, "collecting_outputs")) {
      outputFiles = ImmutableList.builder();
      outputDirectories = ImmutableList.builder();
      requiredData = ImmutableMap.builder();
      if (result.getExitCode() == 0) {
        // TODO(cjhopman): Should outputs be returned on failure?
        collectOutputs(outputs, buildDir, outputFiles, outputDirectories, requiredData);
      }
    }

    return new ActionResult(
        outputFiles.build(),
        outputDirectories.build(),
        requiredData.build(),
        result.getExitCode(),
        result.getStderr().get(),
        result.getStdout().get());
  }

  private void collectOutputs(
      Set<Path> outputs,
      Path buildDir,
      ImmutableList.Builder<OutputFile> outputFilesBuilder,
      ImmutableList.Builder<OutputDirectory> outputDirectoriesBuilder,
      ImmutableMap.Builder<Digest, ThrowingSupplier<InputStream, IOException>> requiredDataBuilder)
      throws IOException {
    for (Path output : outputs) {
      Path path = buildDir.resolve(output);
      Preconditions.checkState(Files.exists(path));
      if (Files.isDirectory(path)) {
        FileTreeBuilder builder = new FileTreeBuilder();

        try (Stream<Path> contents = Files.walk(path)) {
          RichStream.from(contents)
              .forEachThrowing(
                  entry -> {
                    if (Files.isRegularFile(entry)) {
                      builder.addFile(
                          path.relativize(entry),
                          () ->
                              new InputFile(
                                  hashFile(entry).toString(),
                                  (int) Files.size(entry),
                                  Files.isExecutable(entry),
                                  () -> new FileInputStream(entry.toFile())));
                    }
                  });
        }

        List<Directory> directories = new ArrayList<>();
        Digest digest =
            builder.buildTree(
                new ProtocolTreeBuilder(requiredDataBuilder::put, directories::add, protocol));
        Preconditions.checkState(!directories.isEmpty());
        Tree tree = protocol.newTree(directories.get(directories.size() - 1), directories);
        byte[] treeData = protocol.toByteArray(tree);
        Digest treeDigest = protocol.computeDigest(treeData);

        outputDirectoriesBuilder.add(protocol.newOutputDirectory(output, digest, treeDigest));
        requiredDataBuilder.put(treeDigest, () -> new ByteArrayInputStream(treeData));
      } else {
        long size = Files.size(path);
        boolean isExecutable = Files.isExecutable(path);
        Digest digest = protocol.newDigest(hashFile(path).toString(), (int) size);

        ThrowingSupplier<InputStream, IOException> dataSupplier =
            () -> new FileInputStream(path.toFile());
        outputFilesBuilder.add(protocol.newOutputFile(output, digest, isExecutable, dataSupplier));
        requiredDataBuilder.put(digest, dataSupplier);
      }
    }
  }

  private HashCode hashFile(Path file) throws IOException {
    return MoreFiles.asByteSource(file).hash(Hashing.sha1());
  }
}
