// 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.objc;

import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteSource;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration.ConfigurationDistinguisher;
import com.google.devtools.build.lib.rules.apple.Platform.PlatformType;
import com.google.devtools.build.lib.rules.objc.XcodeProvider.Builder;
import com.google.devtools.build.lib.rules.objc.XcodeProvider.Project;
import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos;
import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
import java.io.InputStream;
import java.util.List;

/**
 * Support for Objc rule types that export an Xcode provider or generate xcode project files.
 *
 * <p>Methods on this class can be called in any order without impacting the result.
 *
 * <p>These objects should not outlast the analysis phase. Do not pass them to {@link Action}
 * objects or other persistent objects. There are internal tests to ensure that XcodeSupport objects
 * are not persisted that check the name of this class, so update those tests if you change this
 * class's name.
 */
public final class XcodeSupport {

  /**
   * Template for a target's xcode project.
   */
  public static final SafeImplicitOutputsFunction PBXPROJ =
      fromTemplates("%{name}.xcodeproj/project.pbxproj");

  private final RuleContext ruleContext;
  private final IntermediateArtifacts intermediateArtifacts;
  private final Label xcodeTargetLabel;

  /**
   * Creates a new xcode support for the given context.
   */
  XcodeSupport(RuleContext ruleContext)  {
    this(ruleContext, ObjcRuleClasses.intermediateArtifacts(ruleContext), ruleContext.getLabel());
  }

  /**
   * Creates a new xcode support for the given context and {@link IntermediateArtifacts} with given
   * target label.
   */
  public XcodeSupport(
      RuleContext ruleContext, IntermediateArtifacts intermediateArtifacts,
      Label xcodeTargetLabel) {
    this.ruleContext = ruleContext;
    this.intermediateArtifacts = intermediateArtifacts;
    this.xcodeTargetLabel = xcodeTargetLabel;
  }

  /**
   * Adds xcode project files to the given builder.
   *
   * @return this xcode support
   * @throws InterruptedException 
   */
  XcodeSupport addFilesToBuild(NestedSetBuilder<Artifact> filesToBuild)
      throws InterruptedException {
    filesToBuild.add(ruleContext.getImplicitOutputArtifact(PBXPROJ));
    return this;
  }

  /**
   * Adds a dummy source file to the Xcode target. This is needed if the target does not have any
   * source files but Xcode requires one.
   *
   * @return this xcode support
   */
  XcodeSupport addDummySource(XcodeProvider.Builder xcodeProviderBuilder) {
    ruleContext.registerAction(new SymlinkAction(
        ruleContext.getActionOwner(),
        ruleContext.getPrerequisiteArtifact("$dummy_source", Mode.TARGET),
        intermediateArtifacts.dummySource(),
        "Symlinking dummy artifact"));

    xcodeProviderBuilder.addAdditionalSources(intermediateArtifacts.dummySource());
    return this;
  }

  /**
   * Registers actions that generate the rule's Xcode project.
   *
   * @param xcodeProvider information about this rule's xcode settings and that of its dependencies
   * @return this xcode support
   * @throws InterruptedException 
   */
  XcodeSupport registerActions(XcodeProvider xcodeProvider) throws InterruptedException {
    registerXcodegenActions(XcodeProvider.Project.fromTopLevelTarget(xcodeProvider));
    return this;
  }

  /**
   * Registers actions that generate the rule's Xcode project.
   *
   * @param xcodeProviders information about several rules' xcode settings
   * @return this xcode support
   * @throws InterruptedException 
   */
  XcodeSupport registerActions(Iterable<XcodeProvider> xcodeProviders) throws InterruptedException {
    registerXcodegenActions(Project.fromTopLevelTargets(xcodeProviders));
    return this;
  }

  /**
   * Adds common xcode settings to the given provider builder.
   *
   * @param objcProvider provider containing all dependencies' information as well as some of this
   *    rule's
   * @param productType type of this rule's Xcode target
   *
   * @return this xcode support
   */
  XcodeSupport addXcodeSettings(XcodeProvider.Builder xcodeProviderBuilder,
      ObjcProvider objcProvider, XcodeProductType productType) {
    AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);
    return addXcodeSettings(xcodeProviderBuilder, objcProvider, productType,
        appleConfiguration.getIosCpu(), appleConfiguration.getConfigurationDistinguisher());
  }

  /**
   * Adds common xcode settings to the given provider builder, explicitly specifying architecture
   * to use.
   *
   * @param objcProvider provider containing all dependencies' information as well as some of this
   *    rule's
   * @param productType type of this rule's Xcode target
   * @param architecture architecture to filter all dependencies with (only matching ones will be
   *    included in the final targets generated)
   * @param configurationDistinguisher distinguisher that will cause this target's xcode provider to
   *    discard any dependencies from sources that are tagged with a different distinguisher
   * @return this xcode support
   */
  XcodeSupport addXcodeSettings(Builder xcodeProviderBuilder,
      ObjcProvider objcProvider, XcodeProductType productType, String architecture,
      ConfigurationDistinguisher configurationDistinguisher) {
    xcodeProviderBuilder
        .setLabel(xcodeTargetLabel)
        .setArchitecture(architecture)
        .setConfigurationDistinguisher(configurationDistinguisher)
        .setObjcProvider(objcProvider)
        .setProductType(productType)
        .addXcodeprojBuildSettings(XcodeSupport.defaultXcodeSettings());
    return this;
  }

  /**
   * Adds dependencies to the given provider builder from the given attribute.
   *
   * @return this xcode support
   */
  XcodeSupport addDependencies(Builder xcodeProviderBuilder, Attribute attribute) {
    xcodeProviderBuilder.addPropagatedDependencies(
        ruleContext.getPrerequisites(
            attribute.getName(), attribute.getAccessMode(), XcodeProvider.class));
    return this;
  }

  /**
   * Adds non-propagated dependencies to the given provider builder from the given attribute.
   *
   * <p>A non-propagated dependency will not be linked into the final app bundle and can only serve
   * as a compile-only dependency for its direct dependent.
   *
   * @return this xcode support
   */
  XcodeSupport addNonPropagatedDependencies(Builder xcodeProviderBuilder, Attribute attribute) {
    xcodeProviderBuilder.addNonPropagatedDependencies(
        ruleContext.getPrerequisites(
            attribute.getName(), attribute.getAccessMode(), XcodeProvider.class));
    return this;
  }

  /**
   * Adds J2ObjC JRE dependencies to the given provider builder from the given attribute.
   *
   * @return this xcode support
   */
  XcodeSupport addJreDependencies(Builder xcodeProviderBuilder) {
    xcodeProviderBuilder.addJreDependencies(
        ruleContext.getPrerequisites("jre_deps", Mode.TARGET, XcodeProvider.class));
    return this;
  }

  /**
   * Generates an extra {@link XcodeProductType#LIBRARY_STATIC} Xcode target with the same
   * compilation artifacts as the main Xcode target associated with this Xcode support. The extra
   * Xcode library target, instead of the main Xcode target, will act as a dependency for all
   * dependent Xcode targets.
   *
   * <p>This is needed to build the Xcode binary target generated by ios_application in XCode.
   * Currently there is an Xcode target dependency between the binary target from ios_application
   * and the binary target from objc_binary. But Xcode does not link in compiled artifacts from
   * binary dependencies, so any sources specified on objc_binary rules will not be compiled and
   * linked into the app bundle in dependent binary targets associated with ios_application in
   * XCode.
   */
  // TODO(bazel-team): Remove this when the binary rule types and bundling rule types are merged.
  XcodeSupport generateCompanionLibXcodeTarget(Builder xcodeProviderBuilder) {
    xcodeProviderBuilder.generateCompanionLibTarget();
    return this;
  }

  private void registerXcodegenActions(XcodeProvider.Project project) throws InterruptedException {
    Artifact controlFile = intermediateArtifacts.pbxprojControlArtifact();

    ruleContext.registerAction(new BinaryFileWriteAction(
        ruleContext.getActionOwner(),
        controlFile,
        xcodegenControlFileBytes(project),
        /*makeExecutable=*/false));

    ruleContext.registerAction(new SpawnAction.Builder()
        .setMnemonic("GenerateXcodeproj")
        .setExecutable(ruleContext.getExecutablePrerequisite("$xcodegen", Mode.HOST))
        .addArgument("--control")
        .addInputArgument(controlFile)
        .addOutput(ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ))
        .addTransitiveInputs(project.getInputsToXcodegen())
        .addTransitiveInputs(project.getAdditionalSources())
        .build(ruleContext));
  }

  /**
   * Static class to avoid keeping references to configurations and this XcodeSupport object during
   * execution.
   */
  private static class XcodegenControlFileBytes extends ByteSource {
    private final XcodeProvider.Project project;
    private final Artifact pbxproj;
    private final String workspaceRoot;
    private final List<String> appleCpus;
    private final String minimumOs;
    private final boolean generateDebugSymbols;

    XcodegenControlFileBytes(
        ObjcConfiguration objcConfiguration,
        AppleConfiguration appleConfiguration,
        Project project,
        Artifact pbxproj) {
      this.project = project;
      this.pbxproj = pbxproj;
      this.workspaceRoot = objcConfiguration.getXcodeWorkspaceRoot();
      this.appleCpus = appleConfiguration.getMultiArchitectures(
          appleConfiguration.getSingleArchPlatform().getType());
      this.minimumOs = appleConfiguration.getMinimumOsForPlatformType(PlatformType.IOS).toString();
      this.generateDebugSymbols = objcConfiguration.generateDsym();
    }

    @Override
    public InputStream openStream() {
      XcodeGenProtos.Control.Builder builder = XcodeGenProtos.Control.newBuilder();
      if (workspaceRoot != null) {
        builder.setWorkspaceRoot(workspaceRoot);
      }

      builder.addAllCpuArchitecture(appleCpus);

      return builder
          .setPbxproj(pbxproj.getExecPathString())
          .addAllTarget(project.targets())
          .addBuildSetting(
              XcodeGenProtos.XcodeprojBuildSetting.newBuilder()
                  .setName("IPHONEOS_DEPLOYMENT_TARGET")
                  .setValue(minimumOs)
                  .build())
          .addBuildSetting(
              XcodeGenProtos.XcodeprojBuildSetting.newBuilder()
                  .setName("DEBUG_INFORMATION_FORMAT")
                  .setValue(generateDebugSymbols ? "dwarf-with-dsym" : "dwarf")
                  .build())
          .build()
          .toByteString()
          .newInput();
    }
  }

  private ByteSource xcodegenControlFileBytes(XcodeProvider.Project project)
      throws InterruptedException {
    return new XcodegenControlFileBytes(
        ObjcRuleClasses.objcConfiguration(ruleContext),
        ruleContext.getFragment(AppleConfiguration.class),
        project,
        ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ));
  }

  /**
   * Returns a list of default XCode build settings for Bazel-generated XCode projects.
   */
  @VisibleForTesting
  static Iterable<XcodeprojBuildSetting> defaultXcodeSettings() {
    // Do not use XCode headermap because Bazel-generated header search paths are sufficient for
    // resolving header imports.
    return ImmutableList.of(
        XcodeprojBuildSetting.newBuilder()
            .setName("USE_HEADERMAP")
            .setValue("NO")
            .build());
  }
}
