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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.rules.java.JavaPluginInfoProvider;
import com.google.devtools.build.lib.rules.java.JavaTargetAttributes;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.build.lib.vfs.PathFragment;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Support logic for Bazel's
 * <a href="https://developer.android.com/topic/libraries/data-binding/index.html">data binding</a>
 * integration.
 *
 * <p>In short, data binding in Bazel works as follows:
 * <ol>
 *   <li>If a rule enables data binding and has layout resources with data binding expressions,
 *     resource processing invokes the data binding library to preprocess these expressions, then
 *     strips them out before feeding the resources into aapt. A separate "layout info" XML file
 *     gets produced that contains the bindings.</li>
 *   <li>The data binding annotation processor gets activated on Java compilation. This processor
 *     reads a custom-generated <code>DataBindingInfo.java</code> which specifies the path to the
 *     layout info file (as an annotation). The processor reads that file and produces the
 *     corresponding Java classes that end-user code uses to access the resources.</li>
 *   <li>The data binding compile-time and runtime support libraries get linked into the binary's
 *     deploy jar.</li>
 * </ol>
 *
 * <p>For data binding to work, the corresponding support libraries must be checked into the depot
 * via the implicit dependencies specified inside this class.
 *
 * <p>Unless otherwise specified, all methods in this class assume the current rule applies data
 * binding. Callers can intelligently trigger this logic by checking {@link #isEnabled}.
 *
 */
public final class DataBinding {
  /**
   * The rule attribute supplying data binding's annotation processor.
   */
  public static final String DATABINDING_ANNOTATION_PROCESSOR_ATTR =
      "$databinding_annotation_processor";

  /**
   * Annotation processing creates the following metadata files that describe how data binding is
   * applied. The full file paths include prefixes as implemented in {@link #getMetadataOutputs}.
   */
  private static final ImmutableList<String> METADATA_OUTPUT_SUFFIXES = ImmutableList.<String>of(
      "setter_store.bin", "layoutinfo.bin", "br.bin");

  /**
   * The directory where the annotation processor looks for dep metadata.
   */
  private static final String DEP_METADATA_INPUT_DIR = "dependent-lib-artifacts";

  /**
   * The directory where the annotation processor write metadata output for the current rule.
   */
  private static final String METADATA_OUTPUT_DIR = "bin-files";

  /**
   * Should data binding support be enabled for this rule?
   *
   * <p>This is true if either the rule or any of its transitive dependencies declares data binding
   * support in its attributes.
   *
   * <p>Data binding incurs additional resource processing and compilation work as well as
   * additional compile/runtime dependencies. But rules with data binding disabled will fail if
   * any data binding expressions appear in their layout resources.
   */
  public static boolean isEnabled(RuleContext ruleContext) {
    if (ruleContext.attributes().has("enable_data_binding", Type.BOOLEAN)
        && ruleContext.attributes().get("enable_data_binding", Type.BOOLEAN)) {
      return true;
    } else {
      return !Iterables.isEmpty(ruleContext.getPrerequisites("deps",
          RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class));
    }
  }

  /**
   * Returns this rule's data binding base output dir (as an execroot-relative path).
   */
  private static PathFragment getDataBindingExecPath(RuleContext ruleContext) {
    return ruleContext.getBinOrGenfilesDirectory().getExecPath().getRelative(
        ruleContext.getUniqueDirectory("databinding"));
  }

  /**
   * Returns an artifact for the specified output under a standardized data binding base dir.
   */
  private static Artifact getDataBindingArtifact(RuleContext ruleContext, String relativePath) {
    PathFragment binRelativeBasePath = getDataBindingExecPath(ruleContext)
        .relativeTo(ruleContext.getBinOrGenfilesDirectory().getExecPath());
    return ruleContext.getDerivedArtifact(binRelativeBasePath.getRelative(relativePath),
        ruleContext.getBinOrGenfilesDirectory());
  }

  /**
   * Returns the file where data binding's resource processing produces binding xml. For
   * example, given:
   *
   * <pre>{@code
   *   <layout>
   *     <data>
   *       <variable name="foo" type="String" />
   *     </data>
   *   </layout>
   *   <LinearLayout>
   *     ...
   *   </LinearLayout>
   * }
   * </pre>
   *
   * <p>data binding strips out and processes this part:
   *
   * <pre>{@code
   *     <data>
   *       <variable name="foo" type="String" />
   *     </data>
   * }
   * </pre>
   *
   * for each layout file with data binding expressions. Since this may produce multiple
   * files, outputs are zipped up into a single container.
   */
  static Artifact getLayoutInfoFile(RuleContext ruleContext) {
    // The data binding library expects this to be called "layout-info.zip".
    return ruleContext.getUniqueDirectoryArtifact("databinding", "layout-info.zip",
        ruleContext.getBinOrGenfilesDirectory());
  }

  /**
   * Adds data binding's annotation processor as a plugin to the given Java compilation context.
   *
   * <p>This, in conjunction with {@link #createAnnotationFile} extends the Java compilation to
   * translate data binding .xml into corresponding classes.
   */
  static void addAnnotationProcessor(RuleContext ruleContext,
      JavaTargetAttributes.Builder attributes, boolean isBinary) {
    JavaPluginInfoProvider plugin = ruleContext.getPrerequisite(
        DATABINDING_ANNOTATION_PROCESSOR_ATTR, RuleConfiguredTarget.Mode.TARGET,
        JavaPluginInfoProvider.class);
    for (String name : plugin.getProcessorClasses()) {
      // For header compilation (see JavaHeaderCompileAction):
      attributes.addApiGeneratingProcessorName(name);
      // For full compilation:
      attributes.addProcessorName(name);
    }
    // For header compilation (see JavaHeaderCompileAction):
    attributes.addApiGeneratingProcessorPath(plugin.getProcessorClasspath());
    // For full compilation:
    attributes.addProcessorPath(plugin.getProcessorClasspath());
    attributes.addAdditionalOutputs(getMetadataOutputs(ruleContext));

    addProcessorFlags(ruleContext, attributes, isBinary);
  }

  /**
   * Adds javac flags to configure data binding's annotation processor.
   */
  private static void addProcessorFlags(RuleContext ruleContext,
      JavaTargetAttributes.Builder attributes, boolean isBinary) {
    String metadataOutputDir = getDataBindingExecPath(ruleContext).getPathString();

    // Directory where the annotation processor looks for deps metadata output. The annotation
    // processor automatically appends {@link DEP_METADATA_INPUT_DIR} to this path. Individual
    // files can be anywhere under this directory, recursively.
    addProcessorFlag(attributes, "bindingBuildFolder", metadataOutputDir);
    // Directory where the annotation processor should write this rule's metadata output. The
    // annotation processor automatically appends {@link METADATA_OUTPUT_DIR} to this path.
    addProcessorFlag(attributes, "generationalFileOutDir", metadataOutputDir);
    // Path to the Android SDK installation (if available).
    addProcessorFlag(attributes, "sdkDir", "/not/used");
    // Whether the current rule is a library or binary.
    addProcessorFlag(attributes, "artifactType", isBinary ? "APPLICATION" : "LIBRARY");
    // The path where data binding's resource processor wrote its output (the data binding XML
    // expressions). The annotation processor reads this file to translate that XML into Java.
    addProcessorFlag(attributes, "xmlOutDir", getDataBindingExecPath(ruleContext).toString());
    // Unused.
    addProcessorFlag(attributes, "exportClassListTo", "/tmp/exported_classes");
    // The Java package for the current rule.
    addProcessorFlag(attributes, "modulePackage",
        AndroidCommon.getJavaPackage(ruleContext));
    // The minimum Android SDK compatible with this rule.
    addProcessorFlag(attributes, "minApi", "14"); // TODO(gregce): update this
    // If enabled, the annotation processor reports detailed output about its activities.
    // addProcessorFlag(attributes, "enableDebugLogs", "1");
    // If enabled, produces cleaner output for Android Studio.
    addProcessorFlag(attributes, "printEncodedErrors", "0");
    // Specifies whether the current rule is a test. Currently unused.
    //    addDataBindingProcessorFlag(attributes, "isTestVariant", "false");
    // Specifies that data binding is only used for test instrumentation. Currently unused.
    // addDataBindingProcessorFlag(attributes, "enableForTests", null);
  }

  /**
   * Turns a key/value pair into a javac annotation processor flag received by data binding.
   */
  private static void addProcessorFlag(JavaTargetAttributes.Builder attributes,
      String flag, String value) {
    attributes.addProcessorFlag(String.format("-Aandroid.databinding.%s=%s", flag, value));
  }

  /**
   * Creates and returns the generated Java source that data binding's annotation processor
   * reads to translate layout info xml (from {@link #getLayoutInfoFile} into the classes that
   * end user code consumes.
   *
   * <p>This mostly just triggers the annotation processor. Annotation processor settings
   * are configured in {@link #addProcessorFlags}.
   */
  static Artifact createAnnotationFile(RuleContext ruleContext, boolean isLibrary) {
    String contents;
    try {
      contents = ResourceFileLoader.loadResource(DataBinding.class,
          "databinding_annotation_template.txt");
    } catch (IOException e) {
      ruleContext.ruleError("Cannot load annotation processor template: " + e.getMessage());
      return null;
    }
    Artifact output = getDataBindingArtifact(ruleContext, "DataBindingInfo.java");
    ruleContext.registerAction(FileWriteAction.create(ruleContext, output, contents, false));
    return output;
  }

  /**
   * Adds the appropriate {@link UsesDataBindingProvider} for a rule if it should expose one.
   *
   * <p>A rule exposes {@link UsesDataBindingProvider} if either it or its deps set
   * {@code enable_data_binding = 1}.
   */
  public static void maybeAddProvider(RuleConfiguredTargetBuilder builder,
      RuleContext ruleContext) {
    // Expose the data binding provider if this rule either applies data binding or exports a dep
    // that applies it.
    List<Artifact> dataBindingMetadataOutputs = new ArrayList<>();
    if (DataBinding.isEnabled(ruleContext)) {
      dataBindingMetadataOutputs.addAll(getMetadataOutputs(ruleContext));
    }
    dataBindingMetadataOutputs.addAll(getTransitiveMetadata(ruleContext, "exports"));
    if (!LocalResourceContainer.definesAndroidResources(ruleContext.attributes())) {
      // If this rule doesn't declare direct resources, no resource processing is run so no data
      // binding outputs are produced. In that case, we need to explicitly propagate data binding
      // outputs from the deps to make sure they continue up the build graph.
      dataBindingMetadataOutputs.addAll(getTransitiveMetadata(ruleContext, "deps"));
    }
    if (!dataBindingMetadataOutputs.isEmpty()) {
      builder.addProvider(UsesDataBindingProvider.class,
          new UsesDataBindingProvider(dataBindingMetadataOutputs));
    }
  }

  /**
   * Returns the data binding resource processing output from deps under the given attribute.
   */
  private static List<Artifact> getTransitiveMetadata(RuleContext ruleContext, String attr) {
    ImmutableList.Builder<Artifact> dataBindingMetadataOutputs = ImmutableList.builder();
    if (ruleContext.attributes().has(attr, BuildType.LABEL_LIST)) {
      for (UsesDataBindingProvider provider : ruleContext.getPrerequisites(attr,
          RuleConfiguredTarget.Mode.TARGET, UsesDataBindingProvider.class)) {
        dataBindingMetadataOutputs.addAll(provider.getMetadataOutputs());
      }
    }
    return dataBindingMetadataOutputs.build();
  }

  /**
   * Returns metadata outputs from this rule's annotation processing that describe what it did with
   * data binding. This is used by parent rules to ensure consistent binding patterns.
   *
   * <p>>For example, if {@code foo.AndroidBinary} depends on {@code foo.lib.AndroidLibrary} and
   * the library defines data binding expression {@code Bar}, compiling the library produces Java
   * class {@code foo.lib.Bar}. But since the binary applies data binding over the merged resources
   * of its deps, that means the binary also sees {@code Bar}, so it compiles it into
   * {@code foo.Bar}. This would be a class redefinition conflict. But by feeding the library's
   * metadata outputs into the binary's compilation, enough information is available to only use the
   * first version.
   */
  private static List<Artifact> getMetadataOutputs(RuleContext ruleContext) {
    if (!LocalResourceContainer.definesAndroidResources(ruleContext.attributes())) {
      // If this rule doesn't define local resources, no resource processing was done, so it
      // doesn't produce data binding output.
      return ImmutableList.<Artifact>of();
    }
    ImmutableList.Builder<Artifact> outputs = ImmutableList.<Artifact>builder();
    String javaPackage = AndroidCommon.getJavaPackage(ruleContext);
    for (String suffix : METADATA_OUTPUT_SUFFIXES) {
      // The annotation processor automatically creates files with this naming pattern under the
      // {@code -Aandroid.databinding.generationalFileOutDir} base directory.
      outputs.add(getDataBindingArtifact(ruleContext, String.format("%s/%s-%s-%s",
          METADATA_OUTPUT_DIR, javaPackage, javaPackage, suffix)));
    }
    return outputs.build();
  }

  /**
   * Processes deps that also apply data binding.
   *
   * @param ruleContext the current rule
   * @return the deps' metadata outputs. These need to be staged as compilation inputs to the
   *     current rule.
   */
  static ImmutableList<Artifact> processDeps(RuleContext ruleContext) {
    ImmutableList.Builder<Artifact> dataBindingJavaInputs = ImmutableList.<Artifact>builder();
    if (LocalResourceContainer.definesAndroidResources(ruleContext.attributes())) {
      dataBindingJavaInputs.add(DataBinding.getLayoutInfoFile(ruleContext));
    }
    for (Artifact dataBindingDepMetadata : getTransitiveMetadata(ruleContext, "deps")) {
      dataBindingJavaInputs.add(
          symlinkDepsMetadataIntoOutputTree(ruleContext, dataBindingDepMetadata));
    }
    return dataBindingJavaInputs.build();
  }

  /**
   *
   * Data binding's annotation processor reads the transitive metadata outputs of the target's deps
   * (see {@link #getMetadataOutputs(RuleContext)}) in the directory specified by the processor
   * flag {@code -Aandroid.databinding.bindingBuildFolder}. Since dependencies don't generate
   * their outputs under a common directory, we symlink them into a common place here.
   *
   * @return the symlink paths of the transitive dep metadata outputs for this rule
   */
  private static Artifact symlinkDepsMetadataIntoOutputTree(RuleContext ruleContext,
      Artifact depMetadata) {
    Label ruleLabel = ruleContext.getRule().getLabel();
    Artifact symlink = getDataBindingArtifact(ruleContext,
        String.format("%s/%s", DEP_METADATA_INPUT_DIR, depMetadata.getRootRelativePathString()));
    ruleContext.registerAction(
        new SymlinkAction(ruleContext.getActionOwner(), depMetadata, symlink,
            String.format("Symlinking dep metadata output %s for %s",
                depMetadata.getFilename(), ruleLabel)));
    return symlink;
  }
}

