/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.facebook.buck.features.d;

import com.facebook.buck.core.cell.CellPathResolver;
import com.facebook.buck.core.model.BuildTarget;
import com.facebook.buck.core.model.Flavor;
import com.facebook.buck.core.model.InternalFlavor;
import com.facebook.buck.core.model.TargetConfiguration;
import com.facebook.buck.core.model.impl.BuildTargetPaths;
import com.facebook.buck.core.rules.ActionGraphBuilder;
import com.facebook.buck.core.rules.BuildRule;
import com.facebook.buck.core.rules.BuildRuleParams;
import com.facebook.buck.core.rules.BuildRuleResolver;
import com.facebook.buck.core.rules.SourcePathRuleFinder;
import com.facebook.buck.core.rules.common.BuildableSupport;
import com.facebook.buck.core.rules.impl.SymlinkTree;
import com.facebook.buck.core.sourcepath.SourcePath;
import com.facebook.buck.core.toolchain.ToolchainProvider;
import com.facebook.buck.core.toolchain.tool.Tool;
import com.facebook.buck.core.util.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.cxx.CxxLink;
import com.facebook.buck.cxx.CxxLinkOptions;
import com.facebook.buck.cxx.CxxLinkableEnhancer;
import com.facebook.buck.cxx.config.CxxBuckConfig;
import com.facebook.buck.cxx.toolchain.CxxPlatform;
import com.facebook.buck.cxx.toolchain.CxxPlatformsProvider;
import com.facebook.buck.cxx.toolchain.UnresolvedCxxPlatform;
import com.facebook.buck.cxx.toolchain.linker.Linker;
import com.facebook.buck.cxx.toolchain.nativelink.NativeLinkableGroup;
import com.facebook.buck.cxx.toolchain.nativelink.NativeLinkableInput;
import com.facebook.buck.io.file.MorePaths;
import com.facebook.buck.io.filesystem.ProjectFilesystem;
import com.facebook.buck.rules.args.SourcePathArg;
import com.facebook.buck.rules.args.StringArg;
import com.facebook.buck.rules.coercer.SourceSortedSet;
import com.facebook.buck.util.MoreMaps;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

/** Utility functions for use in D Descriptions. */
abstract class DDescriptionUtils {

  public static final Flavor SOURCE_LINK_TREE = InternalFlavor.of("source-link-tree");

  /**
   * Creates a BuildTarget, based on an existing build target, but flavored with a CxxPlatform and
   * an additional flavor created by combining a prefix and an output file name.
   *
   * @param existingTarget the existing target
   * @param flavorPrefix prefix to be used for added flavor
   * @param fileName filename to be used for added flavor
   * @param cxxPlatform the C++ platform to compile for
   * @return the new BuildTarget
   */
  public static BuildTarget createBuildTargetForFile(
      BuildTarget existingTarget, String flavorPrefix, String fileName, CxxPlatform cxxPlatform) {
    return existingTarget.withAppendedFlavors(
        cxxPlatform.getFlavor(),
        InternalFlavor.of(flavorPrefix + Flavor.replaceInvalidCharacters(fileName)));
  }

  /**
   * Creates a new BuildTarget, based on an existing target, for a file to be compiled.
   *
   * @param existingTarget the existing target
   * @param src the source file to be compiled
   * @param cxxPlatform the C++ platform to compile the file for
   * @return a BuildTarget to compile a D source file to an object file
   */
  public static BuildTarget createDCompileBuildTarget(
      BuildTarget existingTarget, String src, CxxPlatform cxxPlatform) {
    return createBuildTargetForFile(
        existingTarget, "compile-", DCompileStep.getObjectNameForSourceName(src), cxxPlatform);
  }

  /**
   * Creates a {@link NativeLinkableGroup} using sources compiled by the D compiler.
   *
   * @param cellPathResolver
   * @param params build parameters for the build target
   * @param graphBuilder graphBuilder for build rules
   * @param cxxPlatform the C++ platform to compile for
   * @param dBuckConfig the Buck configuration for D
   * @param compilerFlags flags to pass to the compiler
   * @param sources source files to compile
   * @return the new build rule
   */
  public static CxxLink createNativeLinkable(
      CellPathResolver cellPathResolver,
      BuildTarget buildTarget,
      ProjectFilesystem projectFilesystem,
      BuildRuleParams params,
      ActionGraphBuilder graphBuilder,
      CxxPlatform cxxPlatform,
      DBuckConfig dBuckConfig,
      CxxBuckConfig cxxBuckConfig,
      ImmutableList<String> compilerFlags,
      SourceSortedSet sources,
      ImmutableList<String> linkerFlags,
      DIncludes includes) {

    ImmutableList<SourcePath> sourcePaths =
        sourcePathsForCompiledSources(
            buildTarget,
            projectFilesystem,
            params,
            graphBuilder,
            cxxPlatform,
            dBuckConfig,
            compilerFlags,
            sources,
            includes);

    // Return a rule to link the .o for the binary together with its
    // dependencies.
    return CxxLinkableEnhancer.createCxxLinkableBuildRule(
        cxxBuckConfig,
        cxxPlatform,
        projectFilesystem,
        graphBuilder,
        buildTarget,
        Linker.LinkType.EXECUTABLE,
        Optional.empty(),
        BuildTargetPaths.getGenPath(
            projectFilesystem, buildTarget, "%s/" + buildTarget.getShortName()),
        ImmutableList.of(),
        Linker.LinkableDepType.STATIC,
        Optional.empty(),
        CxxLinkOptions.of(),
        FluentIterable.from(params.getBuildDeps())
            .filter(NativeLinkableGroup.class)
            .transform(g -> g.getNativeLinkable(cxxPlatform, graphBuilder)),
        /* cxxRuntimeType */ Optional.empty(),
        /* bundleLoader */ Optional.empty(),
        ImmutableSet.of(),
        ImmutableSet.of(),
        NativeLinkableInput.builder()
            .addAllArgs(StringArg.from(dBuckConfig.getLinkerFlags()))
            .addAllArgs(StringArg.from(linkerFlags))
            .addAllArgs(SourcePathArg.from(sourcePaths))
            .build(),
        Optional.empty(),
        cellPathResolver);
  }

  public static BuildTarget getSymlinkTreeTarget(BuildTarget baseTarget) {
    return baseTarget.withAppendedFlavors(SOURCE_LINK_TREE);
  }

  static UnresolvedCxxPlatform getUnresolvedCxxPlatform(
      ToolchainProvider toolchainProvider,
      TargetConfiguration toolchainTargetConfiguration,
      DBuckConfig dBuckConfig) {
    CxxPlatformsProvider cxxPlatformsProviderFactory =
        toolchainProvider.getByName(
            CxxPlatformsProvider.DEFAULT_NAME,
            toolchainTargetConfiguration,
            CxxPlatformsProvider.class);
    return dBuckConfig
        .getDefaultCxxPlatform()
        .map(InternalFlavor::of)
        .map(flavor -> cxxPlatformsProviderFactory.getUnresolvedCxxPlatforms().getValue(flavor))
        .orElse(cxxPlatformsProviderFactory.getDefaultUnresolvedCxxPlatform());
  }

  static CxxPlatform getCxxPlatform(
      BuildRuleResolver resolver,
      ToolchainProvider toolchainProvider,
      DBuckConfig dBuckConfig,
      TargetConfiguration targetConfiguration) {
    return getUnresolvedCxxPlatform(toolchainProvider, targetConfiguration, dBuckConfig)
        .resolve(resolver, targetConfiguration);
  }

  public static SymlinkTree createSourceSymlinkTree(
      BuildTarget target,
      ProjectFilesystem projectFilesystem,
      SourcePathRuleFinder ruleFinder,
      SourceSortedSet sources) {
    Preconditions.checkState(target.getFlavors().contains(SOURCE_LINK_TREE));
    return new SymlinkTree(
        "d_src",
        target,
        projectFilesystem,
        BuildTargetPaths.getGenPath(projectFilesystem, target, "%s"),
        MoreMaps.transformKeys(
            sources.toNameMap(target, ruleFinder.getSourcePathResolver(), "srcs"),
            MorePaths.toPathFn(projectFilesystem.getRootPath().getFileSystem())));
  }

  private static ImmutableMap<BuildTarget, DLibrary> getTransitiveDLibraryRules(
      Iterable<? extends BuildRule> inputs) {
    ImmutableMap.Builder<BuildTarget, DLibrary> libraries = ImmutableMap.builder();
    new AbstractBreadthFirstTraversal<BuildRule>(inputs) {
      @Override
      public Iterable<BuildRule> visit(BuildRule rule) {
        if (rule instanceof DLibrary) {
          libraries.put(rule.getBuildTarget(), (DLibrary) rule);
          return rule.getBuildDeps();
        }
        return ImmutableSet.of();
      }
    }.start();
    return libraries.build();
  }

  /**
   * Ensures that a DCompileBuildRule exists for the given target, creating a DCompileBuildRule if
   * neccesary.
   *
   * @param baseParams build parameters for the rule
   * @param graphBuilder BuildRuleResolver the rule should be in
   * @param src the source file to be compiled
   * @param compilerFlags flags to pass to the compiler
   * @param compileTarget the target the rule should be for
   * @param dBuckConfig the Buck configuration for D
   * @return the build rule
   */
  public static DCompileBuildRule requireBuildRule(
      BuildTarget compileTarget,
      BuildTarget baseBuildTarget,
      ProjectFilesystem projectFilesystem,
      BuildRuleParams baseParams,
      ActionGraphBuilder graphBuilder,
      DBuckConfig dBuckConfig,
      ImmutableList<String> compilerFlags,
      String name,
      SourcePath src,
      DIncludes includes) {
    return (DCompileBuildRule)
        graphBuilder.computeIfAbsent(
            compileTarget,
            ignored -> {
              Tool compiler = dBuckConfig.getDCompiler();

              Map<BuildTarget, DIncludes> transitiveIncludes = new TreeMap<>();
              transitiveIncludes.put(baseBuildTarget, includes);
              for (Map.Entry<BuildTarget, DLibrary> library :
                  getTransitiveDLibraryRules(baseParams.getBuildDeps()).entrySet()) {
                transitiveIncludes.put(library.getKey(), library.getValue().getIncludes());
              }

              ImmutableSortedSet.Builder<BuildRule> depsBuilder = ImmutableSortedSet.naturalOrder();
              depsBuilder.addAll(BuildableSupport.getDepsCollection(compiler, graphBuilder));
              depsBuilder.addAll(graphBuilder.filterBuildRuleInputs(src));
              for (DIncludes dIncludes : transitiveIncludes.values()) {
                depsBuilder.addAll(dIncludes.getDeps(graphBuilder));
              }
              ImmutableSortedSet<BuildRule> deps = depsBuilder.build();

              return new DCompileBuildRule(
                  compileTarget,
                  projectFilesystem,
                  baseParams.withDeclaredDeps(deps).withoutExtraDeps(),
                  compiler,
                  ImmutableList.<String>builder()
                      .addAll(dBuckConfig.getBaseCompilerFlags())
                      .addAll(compilerFlags)
                      .build(),
                  name,
                  ImmutableSortedSet.of(src),
                  ImmutableList.copyOf(transitiveIncludes.values()));
            });
  }

  /**
   * Generates BuildTargetPaths and BuildRules to compile D sources to object files, and returns a
   * list of SourcePaths referring to the generated object files.
   *
   * @param sources source files to compile
   * @param compilerFlags flags to pass to the compiler
   * @param baseParams build parameters for the compilation
   * @param graphBuilder graphBuilder for build rules
   * @param cxxPlatform the C++ platform to compile for
   * @param dBuckConfig the Buck configuration for D
   * @return SourcePaths of the generated object files
   */
  public static ImmutableList<SourcePath> sourcePathsForCompiledSources(
      BuildTarget baseBuildTarget,
      ProjectFilesystem projectFilesystem,
      BuildRuleParams baseParams,
      ActionGraphBuilder graphBuilder,
      CxxPlatform cxxPlatform,
      DBuckConfig dBuckConfig,
      ImmutableList<String> compilerFlags,
      SourceSortedSet sources,
      DIncludes includes) {
    ImmutableList.Builder<SourcePath> sourcePaths = ImmutableList.builder();
    for (Map.Entry<String, SourcePath> source :
        sources
            .toNameMap(baseBuildTarget, graphBuilder.getSourcePathResolver(), "srcs")
            .entrySet()) {
      BuildTarget compileTarget =
          createDCompileBuildTarget(baseBuildTarget, source.getKey(), cxxPlatform);
      BuildRule rule =
          requireBuildRule(
              compileTarget,
              baseBuildTarget,
              projectFilesystem,
              baseParams,
              graphBuilder,
              dBuckConfig,
              compilerFlags,
              source.getKey(),
              source.getValue(),
              includes);
      sourcePaths.add(rule.getSourcePathToOutput());
    }
    return sourcePaths.build();
  }
}
