// Copyright 2016 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.common.base.Preconditions.checkNotNull;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_FILE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.GENERAL_RESOURCE_DIR;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.GENERAL_RESOURCE_FILE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MERGE_ZIP;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.STRINGS;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.BundlingRule.FAMILIES_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_BUNDLE_ID_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_DEFAULT_PROVISIONING_PROFILE_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_ENTITLEMENTS_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_INFOPLISTS_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_PROVISIONING_PROFILE_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_RESOURCES_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_STRINGS_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchExtensionBundleRule.WATCH_EXT_STRUCTURED_RESOURCES_ATTR;

import com.dd.plist.NSDictionary;
import com.dd.plist.NSObject;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.FileWriteAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
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.ReleaseBundlingSupport.LinkedBinary;
import com.google.devtools.build.lib.rules.objc.TargetDeviceFamily.InvalidFamilyNameException;
import com.google.devtools.build.lib.rules.objc.TargetDeviceFamily.RepeatedFamilyNameException;
import com.google.devtools.build.lib.rules.objc.WatchUtils.WatchOSVersion;
import com.google.devtools.build.lib.syntax.Type;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Contains support methods to build WatchOS1 extension bundles - does normal bundle processing -
 * compiling and linking the binary, resources, plists and creates a final
 * (signed if necessary) bundle.
 */
public class WatchExtensionSupport {

  private final RuleContext ruleContext;
  private final ImmutableSet<Attribute> dependencyAttributes;
  private final IntermediateArtifacts intermediateArtifacts;
  private final String bundleName;
  private final Artifact ipaArtifact;
  private final Artifact watchApplicationBundle;
  private final Attributes attributes;
  private final XcodeProvider watchApplicationXcodeProvider;
  private final ConfigurationDistinguisher configurationDistinguisher;

  WatchExtensionSupport(
      RuleContext ruleContext,
      ImmutableSet<Attribute> dependencyAttributes,
      IntermediateArtifacts intermediateArtifacts,
      String bundleName,
      Artifact ipaArtifact,
      @Nullable Artifact watchApplicationBundle,
      @Nullable XcodeProvider watchApplicationXcodeProvider,
      ConfigurationDistinguisher configurationDistinguisher) {
    this.ruleContext = ruleContext;
    this.dependencyAttributes = dependencyAttributes;
    this.intermediateArtifacts = intermediateArtifacts;
    this.bundleName = bundleName;
    this.ipaArtifact = ipaArtifact;
    this.attributes = new Attributes(ruleContext);
    this.watchApplicationXcodeProvider = checkNotNull(watchApplicationXcodeProvider);
    this.watchApplicationBundle = checkNotNull(watchApplicationBundle);
    this.configurationDistinguisher = configurationDistinguisher;
  }

  void createBundle(NestedSetBuilder<Artifact> filesToBuild,
      ObjcProvider.Builder exposedObjcProviderBuilder, XcodeProvider.Builder xcodeProviderBuilder)
          throws InterruptedException {

    ObjcProvider releaseBundlingObjcProvider = releaseBundlingObjcProvider();

    WatchUtils.addXcodeSettings(ruleContext, xcodeProviderBuilder);

    registerWatchExtensionAutomaticPlistAction();

    ImmutableSet<TargetDeviceFamily> families = attributes.families();

    if (families.isEmpty()) {
      ruleContext.attributeError(FAMILIES_ATTR, ReleaseBundling.INVALID_FAMILIES_ERROR);
    }

    ReleaseBundling.Builder releaseBundling = new ReleaseBundling.Builder()
        .setIpaArtifact(ipaArtifact)
        .setBundleId(attributes.bundleId())
        .setProvisioningProfile(attributes.provisioningProfile())
        .setProvisioningProfileAttributeName(WATCH_EXT_PROVISIONING_PROFILE_ATTR)
        .setTargetDeviceFamilies(families)
        .setIntermediateArtifacts(intermediateArtifacts)
        .setInfoPlistsFromRule(attributes.infoPlists())
        .addInfoplistInput(watchExtensionAutomaticPlist())
        .setEntitlements(attributes.entitlements());

    if (attributes.isBundleIdExplicitySpecified()) {
      releaseBundling.setPrimaryBundleId(attributes.bundleId());
    } else {
      releaseBundling.setFallbackBundleId(attributes.bundleId());
    }

    AppleConfiguration appleConfiguration = ruleContext.getFragment(AppleConfiguration.class);
    
    ReleaseBundlingSupport releaseBundlingSupport =
        new ReleaseBundlingSupport(
            ruleContext,
            releaseBundlingObjcProvider,
            LinkedBinary.DEPENDENCIES_ONLY,
            ReleaseBundlingSupport.EXTENSION_BUNDLE_DIR_FORMAT,
            bundleName,
            WatchUtils.determineMinimumIosVersion(
                appleConfiguration.getMinimumOsForPlatformType(PlatformType.IOS)),
            releaseBundling.build(),
            appleConfiguration.getMultiArchPlatform(PlatformType.IOS));

    releaseBundlingSupport.registerActions(DsymOutputType.APP);
    releaseBundlingSupport.addXcodeSettings(xcodeProviderBuilder);

    releaseBundlingSupport
        .addFilesToBuild(filesToBuild, Optional.of(DsymOutputType.APP))
        .validateResources()
        .validateAttributes()
        .addExportedDebugArtifacts(exposedObjcProviderBuilder, DsymOutputType.APP);

    XcodeSupport xcodeSupport =
        new XcodeSupport(ruleContext)
            .addFilesToBuild(filesToBuild)
            .addXcodeSettings(
                xcodeProviderBuilder,
                releaseBundlingObjcProvider,
                WatchOSVersion.OS1.getExtensionXcodeProductType(),
                ruleContext
                    .getFragment(AppleConfiguration.class)
                    .getDependencySingleArchitecture(),
                configurationDistinguisher)
            .addDummySource(xcodeProviderBuilder);

    for (Attribute attribute : dependencyAttributes) {
      xcodeSupport.addDependencies(xcodeProviderBuilder, attribute);
    }

    // Generate xcodeproj for watch OS 1 extension as the main target with watch application
    // target as the dependency.
    xcodeProviderBuilder.addPropagatedDependencies(
        ImmutableList.of(watchApplicationXcodeProvider));
    xcodeSupport.registerActions(xcodeProviderBuilder.build());
  }

  /**
   * Registers an action to generate a plist containing entries required for watch extension that
   * should be added to the merged plist.
   */
  private void registerWatchExtensionAutomaticPlistAction() {
    List<String> uiRequiredDeviceCapabilities = ImmutableList.of("watch-companion");
    NSDictionary watchExtensionAutomaticEntries = new NSDictionary();
    watchExtensionAutomaticEntries.put("UIRequiredDeviceCapabilities",
        NSObject.wrap(uiRequiredDeviceCapabilities.toArray()));

    ruleContext.registerAction(
        new FileWriteAction(
            ruleContext.getActionOwner(),
            watchExtensionAutomaticPlist(),
            watchExtensionAutomaticEntries.toGnuStepASCIIPropertyList(),
            /*makeExecutable=*/ false));
  }

  private Artifact watchExtensionAutomaticPlist() {
    return ruleContext.getRelatedArtifact(
        ruleContext.getUniqueDirectory("plists"), "-automatic-watchExtensionInfo.plist");
  }

  private ObjcProvider releaseBundlingObjcProvider() {
    ObjcProvider.Builder objcProviderBuilder = new ObjcProvider.Builder();
    // Add dependency providers.
    for (Attribute attribute : dependencyAttributes) {
      objcProviderBuilder.addTransitiveAndPropagate(
            ruleContext.getPrerequisites(
                attribute.getName(), attribute.getAccessMode(), ObjcProvider.class));
    }

    // Expose the generated watch application bundle to the extension bundle.
    objcProviderBuilder.add(MERGE_ZIP, watchApplicationBundle);

    // Add resource files.
    objcProviderBuilder.addAll(GENERAL_RESOURCE_FILE, attributes.resources())
        .addAll(GENERAL_RESOURCE_FILE, attributes.strings())
        .addAll(GENERAL_RESOURCE_DIR, ObjcCommon.xcodeStructuredResourceDirs(
            attributes.structuredResources()))
        .addAll(BUNDLE_FILE, BundleableFile.flattenedRawResourceFiles(attributes.resources()))
        .addAll(
            BUNDLE_FILE,
            BundleableFile.structuredRawResourceFiles(attributes.structuredResources()))
        .addAll(STRINGS, attributes.strings());

    return objcProviderBuilder.build();
  }

  /**
   * Rule attributes used for creating watch application bundle.
   */
  private static class Attributes {
    private final RuleContext ruleContext;

    private Attributes(RuleContext ruleContext) {
      this.ruleContext = ruleContext;
    }

    /**
     * Returns the value of the {@code families} attribute in a form
     * that is more useful than a list of strings. Returns an empty
     * set for any invalid {@code families} attribute value, including
     * an empty list.
     */
    ImmutableSet<TargetDeviceFamily> families() {
      List<String> rawFamilies = ruleContext.attributes().get(
          AppleWatch1ExtensionRule.WATCH_EXT_FAMILIES_ATTR, Type.STRING_LIST);
      try {
        return ImmutableSet.copyOf(TargetDeviceFamily.fromNamesInRule(rawFamilies));
      } catch (InvalidFamilyNameException | RepeatedFamilyNameException e) {
        return ImmutableSet.of();
      }
    }

    @Nullable
    Artifact provisioningProfile() {
      Artifact explicitProvisioningProfile =
          getPrerequisiteArtifact(WATCH_EXT_PROVISIONING_PROFILE_ATTR);
      if (explicitProvisioningProfile != null) {
        return explicitProvisioningProfile;
      }
      return getPrerequisiteArtifact(WATCH_EXT_DEFAULT_PROVISIONING_PROFILE_ATTR);
    }

    String bundleId() {
      Preconditions.checkState(!Strings.isNullOrEmpty(
          ruleContext.attributes().get(WATCH_EXT_BUNDLE_ID_ATTR, Type.STRING)),
          "requires a bundle_id value");
      return ruleContext.attributes().get(WATCH_EXT_BUNDLE_ID_ATTR, Type.STRING);
    }

    ImmutableList<Artifact> infoPlists() {
      return getPrerequisiteArtifacts(WATCH_EXT_INFOPLISTS_ATTR);
    }

    ImmutableList<Artifact> strings() {
      return getPrerequisiteArtifacts(WATCH_EXT_STRINGS_ATTR);
    }

    ImmutableList<Artifact> resources() {
      return getPrerequisiteArtifacts(WATCH_EXT_RESOURCES_ATTR);
    }

    ImmutableList<Artifact> structuredResources() {
      return getPrerequisiteArtifacts(WATCH_EXT_STRUCTURED_RESOURCES_ATTR);
    }

    @Nullable
    Artifact entitlements() {
      return getPrerequisiteArtifact(WATCH_EXT_ENTITLEMENTS_ATTR);
    }

    private boolean isBundleIdExplicitySpecified() {
      return ruleContext.attributes().isAttributeValueExplicitlySpecified(WATCH_EXT_BUNDLE_ID_ATTR);
    }

    private ImmutableList<Artifact> getPrerequisiteArtifacts(String attribute) {
      return ruleContext.getPrerequisiteArtifacts(attribute, Mode.TARGET).list();
    }

    @Nullable
    private Artifact getPrerequisiteArtifact(String attribute) {
      return ruleContext.getPrerequisiteArtifact(attribute, Mode.TARGET);
    }
  }
}
