// 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.devtools.build.lib.rules.objc.AppleWatch1ExtensionRule.WATCH_APP_DEPS_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FLAG;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.HAS_WATCH1_EXTENSION;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MERGE_ZIP;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.WatchApplicationBundleRule.WATCH_APP_NAME_ATTR;

import com.google.common.base.Joiner;
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.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration.ConfigurationDistinguisher;
import com.google.devtools.build.lib.rules.objc.IosExtension.ExtensionSplitArchTransition;
import com.google.devtools.build.lib.rules.objc.WatchUtils.WatchOSVersion;
import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector;
import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
import com.google.devtools.build.lib.syntax.Type;

/**
 * Implementation for {@code apple_watch1_extension}.
 */
public class AppleWatch1Extension implements RuleConfiguredTargetFactory {

  static final SplitTransition<BuildOptions> MINIMUM_OS_AND_SPLIT_ARCH_TRANSITION =
      new ExtensionSplitArchTransition(WatchUtils.MINIMUM_OS_VERSION,
          ConfigurationDistinguisher.WATCH_OS1_EXTENSION);
  private static final ImmutableSet<Attribute> extensionDependencyAttributes =
      ImmutableSet.of(new Attribute("binary", Mode.SPLIT));
  private static final ImmutableSet<Attribute> applicationDependencyAttributes =
      ImmutableSet.of(new Attribute(WATCH_APP_DEPS_ATTR, Mode.SPLIT));

  @Override
  public ConfiguredTarget create(RuleContext ruleContext)
      throws InterruptedException, RuleErrorException {
    ObjcProvider.Builder extensionObjcProviderBuilder = new ObjcProvider.Builder();
    XcodeProvider.Builder applicationXcodeProviderBuilder = new XcodeProvider.Builder();
    XcodeProvider.Builder extensionXcodeProviderBuilder = new XcodeProvider.Builder();
    NestedSetBuilder<Artifact> applicationFilesToBuild = NestedSetBuilder.stableOrder();
    NestedSetBuilder<Artifact> extensionfilesToBuild = NestedSetBuilder.stableOrder();

    // 1. Build watch application bundle.
    createWatchApplicationBundle(
        ruleContext,
        applicationXcodeProviderBuilder,
        applicationFilesToBuild);

    // 2. Build watch extension bundle.
    createWatchExtensionBundle(ruleContext, extensionXcodeProviderBuilder,
        applicationXcodeProviderBuilder, extensionObjcProviderBuilder, extensionfilesToBuild);

    // 3. Extract the watch application bundle into the extension bundle.
    registerWatchApplicationUnBundlingAction(ruleContext);

    RuleConfiguredTargetBuilder targetBuilder =
        ObjcRuleClasses.ruleConfiguredTarget(ruleContext, extensionfilesToBuild.build())
            .addProvider(XcodeProvider.class, extensionXcodeProviderBuilder.build())
            .addProvider(
                InstrumentedFilesProvider.class,
                InstrumentedFilesCollector.forward(ruleContext, "binary"));

    // 4. Exposed {@ObjcProvider} for bundling into final IPA.
    exposeObjcProvider(ruleContext, targetBuilder, extensionObjcProviderBuilder);

    return targetBuilder.build();
  }

  /**
   * Exposes an {@link ObjcProvider} with the following to create the final IPA:
   *    1. Watch extension bundle.
   *    2. WatchKitSupport.
   *    3. A flag to indicate that watch os 1 extension is included.
   */
  private void exposeObjcProvider(
      RuleContext ruleContext,
      RuleConfiguredTargetBuilder targetBuilder,
      ObjcProvider.Builder exposedObjcProviderBuilder)
      throws InterruptedException {
    exposedObjcProviderBuilder.add(MERGE_ZIP,
        ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA));
    WatchUtils.registerActionsToAddWatchSupport(ruleContext, exposedObjcProviderBuilder,
        WatchOSVersion.OS1);
    exposedObjcProviderBuilder.add(FLAG, HAS_WATCH1_EXTENSION);

    targetBuilder.addProvider(ObjcProvider.class, exposedObjcProviderBuilder.build());
  }

  /**
   * Creates a watch extension bundle.
   *
   * @param ruleContext rule context in which to create the bundle
   * @param extensionXcodeProviderBuilder {@link XcodeProvider.Builder} for the extension
   * @param applicationXcodeProviderBuilder {@link XcodeProvider.Builder} for the watch application
   *    which is added as a dependency to the extension
   * @param objcProviderBuilder {@link ObjcProvider.Builder} for the extension
   * @param filesToBuild the list to contain the files to be built for this extension bundle
   */
  private void createWatchExtensionBundle(RuleContext ruleContext,
      XcodeProvider.Builder extensionXcodeProviderBuilder,
      XcodeProvider.Builder applicationXcodeProviderBuilder,
      ObjcProvider.Builder objcProviderBuilder,
      NestedSetBuilder<Artifact> filesToBuild) throws InterruptedException {
    new WatchExtensionSupport(ruleContext,
        extensionDependencyAttributes,
        ObjcRuleClasses.intermediateArtifacts(ruleContext),
        watchExtensionBundleName(ruleContext),
        watchExtensionIpaArtifact(ruleContext),
        watchApplicationBundle(ruleContext),
        applicationXcodeProviderBuilder.build(),
        ConfigurationDistinguisher.WATCH_OS1_EXTENSION)
    .createBundle(filesToBuild, objcProviderBuilder, extensionXcodeProviderBuilder);
  }

  /**
   * Creates a watch application bundle.
   * @param ruleContext rule context in which to create the bundle
   * @param xcodeProviderBuilder {@link XcodeProvider.Builder} for the application
   * @param filesToBuild the list to contain the files to be built for this bundle
   */
  private void createWatchApplicationBundle(
      RuleContext ruleContext,
      XcodeProvider.Builder xcodeProviderBuilder,
      NestedSetBuilder<Artifact> filesToBuild)
      throws InterruptedException {
    new WatchApplicationSupport(
            ruleContext,
            WatchOSVersion.OS1,
            applicationDependencyAttributes,
            new IntermediateArtifacts(ruleContext, "", watchApplicationBundleName(ruleContext)),
            watchApplicationBundleName(ruleContext),
            watchApplicationIpaArtifact(ruleContext),
            watchApplicationBundleName(ruleContext))
        .createBundleAndXcodeproj(
            xcodeProviderBuilder,
            ImmutableList.<Artifact>of(),
            filesToBuild);
  }

  /**
   * Registers action to extract the watch application ipa (after signing if required) to the
   * extension bundle.
   *
   * For example, TestWatchApp.ipa will be unbundled into,
   *   PlugIns/TestWatchExtension.appex
   *   PlugIns/TestWatchExtension.appex/TestWatchApp.app
   */
  private void registerWatchApplicationUnBundlingAction(RuleContext ruleContext) {
    Artifact watchApplicationIpa = watchApplicationIpaArtifact(ruleContext);
    Artifact watchApplicationBundle = watchApplicationBundle(ruleContext);

    String workingDirectory = watchApplicationBundle.getExecPathString().substring(0,
        watchApplicationBundle.getExecPathString().lastIndexOf('/'));

    ImmutableList<String> command = ImmutableList.of(
        "mkdir -p " + workingDirectory,
        "&&",
        String.format("/usr/bin/unzip -q -o %s -d %s",
            watchApplicationIpa.getExecPathString(),
            workingDirectory),
        "&&",
        String.format("cd %s/Payload", workingDirectory),
        "&&",
        String.format("/usr/bin/zip -q -r -0 ../%s *", watchApplicationBundle.getFilename()));
    ruleContext.registerAction(
        ObjcRuleClasses.spawnOnDarwinActionBuilder()
            .setProgressMessage("Extracting watch app: " + ruleContext.getLabel())
            .setShellCommand(ImmutableList.of("/bin/bash", "-c", Joiner.on(" ").join(command)))
            .addInput(watchApplicationIpa)
            .addOutput(watchApplicationBundle)
            .build(ruleContext));
  }

  /**
   * Returns a zip {@Artifact} containing extracted watch application - "TestWatchApp.app"
   * which is to be merged into the extension bundle.
   */
  private Artifact watchApplicationBundle(RuleContext ruleContext) {
    return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory(
        "_watch"), String.format("/%s", watchApplicationIpaArtifact(ruleContext)
            .getFilename().replace(".ipa", ".zip")));
  }

  /**
   * Returns the {@Artifact} containing final watch application bundle.
   */
  private Artifact watchApplicationIpaArtifact(RuleContext ruleContext) {
    return ruleContext.getRelatedArtifact(ruleContext.getUniqueDirectory("_watch"),
          String.format("/%s.ipa", watchApplicationBundleName(ruleContext)));
  }

  /**
   * Returns the {@Artifact} containing final watch extension bundle.
   */
  private Artifact watchExtensionIpaArtifact(RuleContext ruleContext) throws InterruptedException {
    return ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA);
  }

  private String watchApplicationBundleName(RuleContext ruleContext) {
    return ruleContext.attributes().get(WATCH_APP_NAME_ATTR, Type.STRING);
  }

  private String watchExtensionBundleName(RuleContext ruleContext) {
    return ruleContext.getLabel().getName();
  }
}
