// Copyright 2015 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.apple;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.shell.AbnormalTerminationException;
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.CommandResult;
import com.google.devtools.build.lib.shell.TerminationStatus;
import com.google.devtools.build.lib.vfs.Path;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * Obtains information pertaining to Apple host machines required for using Apple toolkits in
 * local action execution.
 */
public class AppleHostInfo {

  private static final String XCRUN_CACHE_FILENAME = "__xcruncache";
  private static final String XCODE_LOCATOR_CACHE_FILENAME = "__xcodelocatorcache";

  /**
   * Returns the absolute root path of the target Apple SDK on the host system for a given
   * version of xcode (as defined by the given {@code developerDir}). This may spawn a
   * process and use the {@code /usr/bin/xcrun} binary to locate the target SDK. This uses a local
   * cache file under {@code bazel-out}, and will only spawn a new {@code xcrun} process in the case
   * of a cache miss.
   *
   * @param execRoot the execution root path, used to locate the cache file
   * @param developerDir the value of {@code DEVELOPER_DIR} for the target version of xcode
   * @param sdkVersion the sdk version, for example, "9.1"
   * @param appleSdkPlatform the sdk platform, for example, "iPhoneOS"
   * @param productName the product name
   * @throws UserExecException if there is an issue with obtaining the root from the spawned
   *     process, either because the SDK platform/version pair doesn't exist, or there was an
   *     unexpected issue finding or running the tool
   */
  public static String getSdkRoot(Path execRoot, String developerDir,
      String sdkVersion, String appleSdkPlatform, String productName) throws UserExecException {
    try {
      CacheManager cacheManager =
          new CacheManager(execRoot.getRelative(
              BlazeDirectories.getRelativeOutputPath(productName)),
              XCRUN_CACHE_FILENAME);

      String sdkString = appleSdkPlatform.toLowerCase() + sdkVersion;
      String cacheResult = cacheManager.getValue(developerDir, sdkString);
      if (cacheResult != null) {
        return cacheResult;
      } else {
        Map<String, String> env = Strings.isNullOrEmpty(developerDir)
            ? ImmutableMap.<String, String>of() : ImmutableMap.of("DEVELOPER_DIR", developerDir);
        CommandResult xcrunResult = new Command(
            new String[] {"/usr/bin/xcrun", "--sdk", sdkString, "--show-sdk-path"},
            env, null).execute();

        // calling xcrun via Command returns a value with a newline on the end.
        String sdkRoot = new String(xcrunResult.getStdout(), StandardCharsets.UTF_8).trim();

        cacheManager.writeEntry(ImmutableList.of(developerDir, sdkString), sdkRoot);
        return sdkRoot;
      }
    } catch (AbnormalTerminationException e) {
      TerminationStatus terminationStatus = e.getResult().getTerminationStatus();

      if (terminationStatus.exited()) {
        throw new UserExecException(
            String.format("xcrun failed with code %s.\n"
                + "This most likely indicates that SDK version [%s] for platform [%s] is "
                + "unsupported for the target version of xcode.\n"
                + "%s\n"
                + "Stderr: %s",
                terminationStatus.getExitCode(),
                sdkVersion, appleSdkPlatform,
                terminationStatus.toString(),
                new String(e.getResult().getStderr(), StandardCharsets.UTF_8)));
      }
      String message = String.format("xcrun failed.\n%s\n%s",
          e.getResult().getTerminationStatus(),
          new String(e.getResult().getStderr(), StandardCharsets.UTF_8));
      throw new UserExecException(message, e);
    } catch (CommandException | IOException e) {
      throw new UserExecException(e);
    }
  }

  /**
   * Returns the absolute root path of the xcode developer directory on the host system for
   * the given xcode version. This may spawn a process and use the {@code xcode-locator} binary.
   * This uses a local cache file under {@code bazel-out}, and will only spawn a new process in the
   * case of a cache miss.
   *
   * @param execRoot the execution root path, used to locate the cache file
   * @param version the xcode version number to look up
   * @param productName the product name
   * @throws UserExecException if there is an issue with obtaining the path from the spawned
   *     process, either because there is no installed xcode with the given version, or
   *     there was an unexpected issue finding or running the tool
   */
  public static String getDeveloperDir(Path execRoot, DottedVersion version, String productName)
      throws UserExecException {
    try {
      CacheManager cacheManager =
          new CacheManager(execRoot.getRelative(BlazeDirectories.getRelativeOutputPath(productName)),
              XCODE_LOCATOR_CACHE_FILENAME);

      String cacheResult = cacheManager.getValue(version.toString());
      if (cacheResult != null) {
        return cacheResult;
      } else {
        CommandResult xcodeLocatorResult = new Command(new String[] {
            execRoot.getRelative("_bin/xcode-locator").getPathString(), version.toString()})
            .execute();

        String developerDir =
            new String(xcodeLocatorResult.getStdout(), StandardCharsets.UTF_8).trim();

        cacheManager.writeEntry(ImmutableList.of(version.toString()), developerDir);
        return developerDir;
      }
    } catch (AbnormalTerminationException e) {
      TerminationStatus terminationStatus = e.getResult().getTerminationStatus();

      String message;
      if (e.getResult().getTerminationStatus().exited()) {
        message = String.format("xcode-locator failed with code %s.\n"
            + "This most likely indicates that xcode version %s is not available on the host "
            + "machine.\n"
            + "%s\n"
            + "stderr: %s",
            terminationStatus.getExitCode(),
            version,
            terminationStatus.toString(),
            new String(e.getResult().getStderr(), StandardCharsets.UTF_8));
      } else {
        message = String.format("xcode-locator failed. %s\nstderr: %s",
            e.getResult().getTerminationStatus(),
            new String(e.getResult().getStderr(), StandardCharsets.UTF_8));
      }
      throw new UserExecException(message, e);
    } catch (CommandException | IOException e) {
      throw new UserExecException(e);
    }
  }
}
