// 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.rules.cpp;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.analysis.RedirectChaser;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.packages.NoSuchThingException;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CrosstoolRelease;
import com.google.protobuf.TextFormat;
import com.google.protobuf.TextFormat.ParseException;
import com.google.protobuf.UninitializedMessageException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;

/**
 * A loader that reads Crosstool configuration files and creates CToolchain
 * instances from them.
 *
 * <p>Note that this class contains a cache for the text format -> proto objects mapping of
 * Crosstool protos that is completely independent from Skyframe or anything else. This should be
 * done in a saner way.
 */
public class CrosstoolConfigurationLoader {
  private static final String CROSSTOOL_CONFIGURATION_FILENAME = "CROSSTOOL";

  /**
   * Cache for storing result of toReleaseConfiguration function based on path and md5 sum of
   * input file. We can use md5 because result of this function depends only on the file content.
   */
  private static final Cache<String, CrosstoolRelease> crosstoolReleaseCache =
      CacheBuilder.newBuilder().concurrencyLevel(4).maximumSize(100).build();
  /**
   * A class that holds the results of reading a CROSSTOOL file.
   */
  public static class CrosstoolFile {
    private final String location;
    private final CrosstoolConfig.CrosstoolRelease crosstool;
    private final String md5;

    CrosstoolFile(String location, CrosstoolConfig.CrosstoolRelease crosstool, String md5) {
      this.location = location;
      this.crosstool = crosstool;
      this.md5 = md5;
    }

    /**
     * Returns a user-friendly location of the CROSSTOOL proto for error messages.
     */
    public String getLocation() {
      return location;
    }

    /**
     * Returns the parsed contents of the CROSSTOOL file.
     */
    public CrosstoolConfig.CrosstoolRelease getProto() {
      return crosstool;
    }

    /**
     * Returns an MD5 hash of the CROSSTOOL file contents.
     */
    public String getMd5() {
      return md5;
    }
  }

  private CrosstoolConfigurationLoader() {
  }

  /**
   * Reads the given <code>data</code> String, which must be in ascii format,
   * into a protocol buffer. It uses the <code>name</code> parameter for error
   * messages.
   *
   * @throws IOException if the parsing failed
   */
  @VisibleForTesting
  static CrosstoolConfig.CrosstoolRelease toReleaseConfiguration(String name, String data)
      throws IOException {
    CrosstoolConfig.CrosstoolRelease.Builder builder =
        CrosstoolConfig.CrosstoolRelease.newBuilder();
    try {
      TextFormat.merge(data, builder);
      return builder.build();
    } catch (ParseException e) {
      throw new IOException("Could not read the crosstool configuration file '" + name + "', "
          + "because of a parser error (" + e.getMessage() + ")");
    } catch (UninitializedMessageException e) {
      throw new IOException("Could not read the crosstool configuration file '" + name + "', "
          + "because of an incomplete protocol buffer (" + e.getMessage() + ")");
    }
  }

  /**
   * This class is the in-memory representation of a text-formatted Crosstool proto file.
   *
   * <p>This layer of abstraction is here so that we can load these protos either from BUILD files
   * or from CROSSTOOL files.
   *
   * <p>An implementation of this class should override {@link #getContents()} and call
   * the constructor with the MD5 checksum of what that method will return and a human-readable name
   * used in error messages.
   */
  private abstract static class CrosstoolProto {
    private final byte[] md5;
    private final String name;

    private CrosstoolProto(byte[] md5, String name) {
      this.md5 = md5;
      this.name = name;
    }

    /**
     * The binary MD5 checksum of the proto.
     */
    public byte[] getMd5() {
      return md5;
    }

    /**
     * A user-friendly string describing the location of the proto.
     */
    public String getName() {
      return name;
    }

    /**
     * The proto itself.
     */
    public abstract String getContents() throws IOException;
  }

  private static CrosstoolProto getCrosstoolProtofromBuildFile(
      ConfigurationEnvironment env, Label crosstoolTop) throws InterruptedException {
    Target target;
    try {
      target = env.getTarget(crosstoolTop);
    } catch (NoSuchThingException e) {
      throw new IllegalStateException(e);  // Should have beeen evaluated by RedirectChaser
    }

    if (!(target instanceof Rule)) {
      return null;
    }

    Rule rule = (Rule) target;
    if (!(rule.getRuleClass().equals("cc_toolchain_suite"))
        || !rule.isAttributeValueExplicitlySpecified("proto")) {
      return null;
    }

    final String contents = NonconfigurableAttributeMapper.of(rule).get("proto", Type.STRING);
    byte[] md5 = new Fingerprint().addBytes(contents.getBytes(UTF_8)).digestAndReset();
    return new CrosstoolProto(md5, "cc_toolchain_suite rule " + crosstoolTop.toString()) {

      @Override
      public String getContents() throws IOException {
        return contents;
      }
    };
  }

  private static CrosstoolProto getCrosstoolProtoFromCrosstoolFile(
      ConfigurationEnvironment env, Label crosstoolTop)
      throws IOException, InvalidConfigurationException, InterruptedException {
    final Path path;
    try {
      Package containingPackage = env.getTarget(crosstoolTop.getLocalTargetLabel("BUILD"))
          .getPackage();
      if (containingPackage == null) {
        return null;
      }
      path = env.getPath(containingPackage, CROSSTOOL_CONFIGURATION_FILENAME);
    } catch (LabelSyntaxException e) {
      throw new InvalidConfigurationException(e);
    } catch (NoSuchThingException e) {
      // Handled later
      return null;
    }

    if (path == null || !path.exists()) {
      return null;
    }

    return new CrosstoolProto(path.getDigest(), "CROSSTOOL file " + path.getPathString()) {
      @Override
      public String getContents() throws IOException {
        try (InputStream inputStream = path.getInputStream()) {
          return new String(FileSystemUtils.readContentAsLatin1(inputStream));
        }
      }
    };
  }

  private static CrosstoolFile findCrosstoolConfiguration(
      ConfigurationEnvironment env, Label crosstoolTop)
      throws IOException, InvalidConfigurationException, InterruptedException {

    CrosstoolProto crosstoolProto = getCrosstoolProtofromBuildFile(env, crosstoolTop);
    if (crosstoolProto == null) {
      crosstoolProto = getCrosstoolProtoFromCrosstoolFile(env, crosstoolTop);
    }

    if (crosstoolProto == null) {
      throw new InvalidConfigurationException("The crosstool_top you specified was resolved to '" +
          crosstoolTop + "', which does not contain a CROSSTOOL file. " +
          "You can use a crosstool from the depot by specifying its label.");
    } else {
      // Do this before we read the data, so if it changes, we get a different MD5 the next time.
      // Alternatively, we could calculate the MD5 of the contents, which we also read, but this
      // is faster if the file comes from a file system with md5 support.
      final CrosstoolProto finalProto = crosstoolProto;
      String md5 = BaseEncoding.base16().lowerCase().encode(finalProto.getMd5());
      CrosstoolConfig.CrosstoolRelease release;
      try {
        release = crosstoolReleaseCache.get(md5, new Callable<CrosstoolRelease>() {
          @Override
          public CrosstoolRelease call() throws Exception {
            return toReleaseConfiguration(finalProto.getName(), finalProto.getContents());
          }
        });
      } catch (ExecutionException e) {
        throw new InvalidConfigurationException(e);
      }

      return new CrosstoolFile(finalProto.getName(), release, md5);
    }
  }

  /** Reads a crosstool file. */
  @Nullable
  public static CrosstoolConfigurationLoader.CrosstoolFile readCrosstool(
      ConfigurationEnvironment env, Label crosstoolTop)
      throws InvalidConfigurationException, InterruptedException {
    crosstoolTop = RedirectChaser.followRedirects(env, crosstoolTop, "crosstool_top");
    if (crosstoolTop == null) {
      return null;
    }
    try {
      return findCrosstoolConfiguration(env, crosstoolTop);
    } catch (IOException e) {
      throw new InvalidConfigurationException(e);
    }
  }

  /**
   * Selects a crosstool toolchain corresponding to the given crosstool
   * configuration options. If all of these options are null, it returns the default
   * toolchain specified in the crosstool release. If only cpu is non-null, it
   * returns the default toolchain for that cpu, as specified in the crosstool
   * release. Otherwise, all values must be non-null, and this method
   * returns the toolchain which matches all of the values.
   *
   * @throws NullPointerException if {@code release} is null
   * @throws InvalidConfigurationException if no matching toolchain can be found, or
   *     if the input parameters do not obey the constraints described above
   */
  public static CrosstoolConfig.CToolchain selectToolchain(
      CrosstoolConfig.CrosstoolRelease release, BuildOptions options,
      Function<String, String> cpuTransformer)
          throws InvalidConfigurationException {
    CrosstoolConfigurationIdentifier config =
        CrosstoolConfigurationIdentifier.fromOptions(options);
    if ((config.getCompiler() != null) || (config.getLibc() != null)) {
      ArrayList<CrosstoolConfig.CToolchain> candidateToolchains = new ArrayList<>();
      for (CrosstoolConfig.CToolchain toolchain : release.getToolchainList()) {
        if (config.isCandidateToolchain(toolchain)) {
          candidateToolchains.add(toolchain);
        }
      }
      switch (candidateToolchains.size()) {
        case 0: {
          StringBuilder message = new StringBuilder();
          message.append("No toolchain found for");
          message.append(config.describeFlags());
          message.append(". Valid toolchains are: ");
          describeToolchainList(message, release.getToolchainList());
          throw new InvalidConfigurationException(message.toString());
        }
        case 1:
          return candidateToolchains.get(0);
        default: {
          StringBuilder message = new StringBuilder();
          message.append("Multiple toolchains found for");
          message.append(config.describeFlags());
          message.append(": ");
          describeToolchainList(message, candidateToolchains);
          throw new InvalidConfigurationException(message.toString());
        }
      }
    }
    String selectedIdentifier = null;
    // We use fake CPU values to allow cross-platform builds for other languages that use the
    // C++ toolchain. Translate to the actual target architecture.
    String desiredCpu = cpuTransformer.apply(config.getCpu());
    for (CrosstoolConfig.DefaultCpuToolchain selector : release.getDefaultToolchainList()) {
      if (selector.getCpu().equals(desiredCpu)) {
        selectedIdentifier = selector.getToolchainIdentifier();
        break;
      }
    }

    if (selectedIdentifier == null) {
      StringBuilder cpuBuilder = new StringBuilder();
      for (CrosstoolConfig.DefaultCpuToolchain selector : release.getDefaultToolchainList()) {
        cpuBuilder.append("  ").append(selector.getCpu()).append(",\n");
      }
      throw new InvalidConfigurationException(
          "No toolchain found for cpu '" + desiredCpu
          + "'. Valid cpus are: [\n" + cpuBuilder + "]");
    }
    checkToolChain(selectedIdentifier, desiredCpu);

    for (CrosstoolConfig.CToolchain toolchain : release.getToolchainList()) {
      if (toolchain.getToolchainIdentifier().equals(selectedIdentifier)) {
        return toolchain;
      }
    }

    throw new InvalidConfigurationException("Inconsistent crosstool configuration; no toolchain "
        + "corresponding to '" + selectedIdentifier + "' found for cpu '" + config.getCpu() + "'");
  }

  /**
   * Appends a series of toolchain descriptions (as the blaze command line flags
   * that would specify that toolchain) to 'message'.
   */
  private static void describeToolchainList(StringBuilder message,
      Collection<CrosstoolConfig.CToolchain> toolchains) {
    message.append("[\n");
    for (CrosstoolConfig.CToolchain toolchain : toolchains) {
      message.append("  ");
      message.append(
          CrosstoolConfigurationIdentifier.fromToolchain(toolchain).describeFlags().trim());
      message.append(",\n");
    }
    message.append("]");
  }

  /**
   * Makes sure that {@code selectedIdentifier} is a valid identifier for a toolchain,
   * i.e. it starts with a letter or an underscore and continues with only dots, dashes,
   * spaces, letters, digits or underscores (i.e. matches the following regular expression:
   * "[a-zA-Z_][\.\- \w]*").
   *
   * @throws InvalidConfigurationException if selectedIdentifier does not match the
   *         aforementioned regular expression.
   */
  private static void checkToolChain(String selectedIdentifier, String cpu)
      throws InvalidConfigurationException {
    // If you update this regex, please do so in the javadoc comment too, and also in the
    // crosstool_config.proto file.
    String rx = "[a-zA-Z_][\\.\\- \\w]*";
    if (!selectedIdentifier.matches(rx)) {
      throw new InvalidConfigurationException(String.format(
          "Toolchain identifier '%s' for cpu '%s' is illegal (does not match '%s')",
          selectedIdentifier, cpu, rx));
    }
  }

  public static CrosstoolConfig.CrosstoolRelease getCrosstoolReleaseProto(
      ConfigurationEnvironment env,
      BuildOptions options,
      Label crosstoolTop,
      Function<String, String> cpuTransformer)
      throws InvalidConfigurationException, InterruptedException {
    CrosstoolConfigurationLoader.CrosstoolFile file =
        readCrosstool(env, crosstoolTop);
    // Make sure that we have the requested toolchain in the result. Throw an exception if not.
    selectToolchain(file.getProto(), options, cpuTransformer);
    return file.getProto();
  }
}
