// Copyright 2014 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.cpp;

import com.google.common.base.Function;
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.Mode;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
import com.google.devtools.build.lib.util.Preconditions;

import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
 * Parameters to be passed to the linker.
 *
 * <p>The parameters concerned are the link options (strings) passed to the linker, linkstamps, a
 * list of libraries to be linked in, and a list of libraries to build at link time.
 *
 * <p>Items in the collections are stored in nested sets. Link options and libraries are stored in
 * link order (preorder) and linkstamps are sorted.
 */
public final class CcLinkParams {
  private final NestedSet<ImmutableList<String>> linkOpts;
  private final NestedSet<Linkstamp> linkstamps;
  private final NestedSet<LibraryToLink> libraries;
  private final ExtraLinkTimeLibraries extraLinkTimeLibraries;

  private CcLinkParams(NestedSet<ImmutableList<String>> linkOpts,
                       NestedSet<Linkstamp> linkstamps,
                       NestedSet<LibraryToLink> libraries,
                       ExtraLinkTimeLibraries extraLinkTimeLibraries) {
    this.linkOpts = linkOpts;
    this.linkstamps = linkstamps;
    this.libraries = libraries;
    this.extraLinkTimeLibraries = extraLinkTimeLibraries;
  }

  /**
   * @return the linkopts
   */
  public NestedSet<ImmutableList<String>> getLinkopts() {
    return linkOpts;
  }

  public ImmutableList<String> flattenedLinkopts() {
    return ImmutableList.copyOf(Iterables.concat(linkOpts));
  }

  /**
   * @return the linkstamps
   */
  public NestedSet<Linkstamp> getLinkstamps() {
    return linkstamps;
  }

  /**
   * @return the libraries
   */
  public NestedSet<LibraryToLink> getLibraries() {
    return libraries;
  }

  /**
   * The extra link time libraries; will be null if there are no such libraries.
   */
  public ExtraLinkTimeLibraries getExtraLinkTimeLibraries() {
    return extraLinkTimeLibraries;
  }

  public static final Builder builder(boolean linkingStatically, boolean linkShared) {
    return new Builder(linkingStatically, linkShared);
  }

  /**
   * Builder for {@link CcLinkParams}.
   *
 *
   */
  public static final class Builder {

    /**
     * linkingStatically is true when we're linking this target in either FULLY STATIC mode
     * (linkopts=["-static"]) or MOSTLY STATIC mode (linkstatic=1). When this is true, we want to
     * use static versions of any libraries that this target depends on (except possibly system
     * libraries, which are not handled by CcLinkParams). When this is false, we want to use dynamic
     * versions of any libraries that this target depends on.
     */
    private final boolean linkingStatically;

    /**
     * linkShared is true when we're linking with "-shared" (linkshared=1).
     */
    private final boolean linkShared;

    private ImmutableList.Builder<String> localLinkoptsBuilder = ImmutableList.builder();

    private final NestedSetBuilder<ImmutableList<String>> linkOptsBuilder =
        NestedSetBuilder.linkOrder();
    private final NestedSetBuilder<Linkstamp> linkstampsBuilder =
        NestedSetBuilder.compileOrder();
    private final NestedSetBuilder<LibraryToLink> librariesBuilder =
        NestedSetBuilder.linkOrder();

    /**
     * A builder for the list of link time libraries.  Most builds
     * won't have any such libraries, so save space by leaving the
     * default as null.
     */
    private ExtraLinkTimeLibraries.Builder extraLinkTimeLibrariesBuilder = null;

    private boolean built = false;

    private Builder(boolean linkingStatically, boolean linkShared) {
      this.linkingStatically = linkingStatically;
      this.linkShared = linkShared;
    }

    /**
     * Build a {@link CcLinkParams} object.
     */
    public CcLinkParams build() {
      Preconditions.checkState(!built);
      // Not thread-safe, but builders should not be shared across threads.
      built = true;
      ImmutableList<String> localLinkopts = localLinkoptsBuilder.build();
      if (!localLinkopts.isEmpty()) {
        linkOptsBuilder.add(localLinkopts);
      }
      ExtraLinkTimeLibraries extraLinkTimeLibraries = null;
      if (extraLinkTimeLibrariesBuilder != null) {
        extraLinkTimeLibraries = extraLinkTimeLibrariesBuilder.build();
      }
      return new CcLinkParams(linkOptsBuilder.build(), linkstampsBuilder.build(),
          librariesBuilder.build(), extraLinkTimeLibraries);
    }

    public boolean add(CcLinkParamsStore store) {
      if (store != null) {
        CcLinkParams args = store.get(linkingStatically, linkShared);
        addTransitiveArgs(args);
      }
      return store != null;
    }

    /**
     * Includes link parameters from a collection of dependency targets.
     */
    public Builder addTransitiveTargets(Iterable<? extends TransitiveInfoCollection> targets) {
      for (TransitiveInfoCollection target : targets) {
        addTransitiveTarget(target);
      }
      return this;
    }

    /**
     * Includes link parameters from a dependency target.
     *
     * <p>The target should implement {@link CcLinkParamsProvider}. If it does not,
     * the method does not do anything.
     */
    public Builder addTransitiveTarget(TransitiveInfoCollection target) {
      return addTransitiveProvider(target.getProvider(CcLinkParamsProvider.class));
    }

    /**
     * Includes link parameters from a dependency target. The target is checked for the given
     * mappings in the order specified, and the first mapping that returns a non-null result is
     * added.
     */
    @SafeVarargs
    public final Builder addTransitiveTarget(TransitiveInfoCollection target,
        Function<TransitiveInfoCollection, CcLinkParamsStore> firstMapping,
        @SuppressWarnings("unchecked") // Java arrays don't preserve generic arguments.
        Function<TransitiveInfoCollection, CcLinkParamsStore>... remainingMappings) {
      if (add(firstMapping.apply(target))) {
        return this;
      }
      for (Function<TransitiveInfoCollection, CcLinkParamsStore> mapping : remainingMappings) {
        if (add(mapping.apply(target))) {
          return this;
        }
      }
      return this;
    }

    /**
     * Includes link parameters from a CcLinkParamsProvider provider.
     */
    public Builder addTransitiveProvider(CcLinkParamsProvider provider) {
      if (provider != null) {
        add(provider.getCcLinkParamsStore());
      }
      return this;
    }

    /**
     * Includes link parameters from the given targets. Each target is checked for the given
     * mappings in the order specified, and the first mapping that returns a non-null result is
     * added.
     */
    @SafeVarargs
    public final Builder addTransitiveTargets(
        Iterable<? extends TransitiveInfoCollection> targets,
        Function<TransitiveInfoCollection, CcLinkParamsStore> firstMapping,
        @SuppressWarnings("unchecked")  // Java arrays don't preserve generic arguments.
        Function<TransitiveInfoCollection, CcLinkParamsStore>... remainingMappings) {
      for (TransitiveInfoCollection target : targets) {
        addTransitiveTarget(target, firstMapping, remainingMappings);
      }
      return this;
    }

    /**
     * Merges the other {@link CcLinkParams} object into this one.
     */
    public Builder addTransitiveArgs(CcLinkParams args) {
      linkOptsBuilder.addTransitive(args.getLinkopts());
      linkstampsBuilder.addTransitive(args.getLinkstamps());
      librariesBuilder.addTransitive(args.getLibraries());
      if (args.getExtraLinkTimeLibraries() != null) {
        if (extraLinkTimeLibrariesBuilder == null) {
          extraLinkTimeLibrariesBuilder = ExtraLinkTimeLibraries.builder();
        }
        extraLinkTimeLibrariesBuilder.addTransitive(args.getExtraLinkTimeLibraries());
      }
      return this;
    }

    /**
     * Adds a collection of link options.
     */
    public Builder addLinkOpts(Collection<String> linkOpts) {
      localLinkoptsBuilder.addAll(linkOpts);
      return this;
    }

    /**
     * Adds a collection of linkstamps.
     */
    public Builder addLinkstamps(NestedSet<Artifact> linkstamps, CppCompilationContext context) {
      for (Artifact linkstamp : linkstamps) {
        linkstampsBuilder.add(new Linkstamp(linkstamp, context.getDeclaredIncludeSrcs()));
      }
      return this;
    }

    /**
     * Adds a library artifact.
     */
    public Builder addLibrary(LibraryToLink library) {
      librariesBuilder.add(library);
      return this;
    }

    /**
     * Adds a collection of library artifacts.
     */
    public Builder addLibraries(Iterable<LibraryToLink> libraries) {
      librariesBuilder.addAll(libraries);
      return this;
    }

    /**
     * Adds an extra link time library, a library that is actually
     * built at link time.
     */
    public Builder addExtraLinkTimeLibrary(ExtraLinkTimeLibrary e) {
      if (extraLinkTimeLibrariesBuilder == null) {
        extraLinkTimeLibrariesBuilder = ExtraLinkTimeLibraries.builder();
      }
      extraLinkTimeLibrariesBuilder.add(e);
      return this;
    }

    /**
     * Processes typical dependencies a C/C++ library.
     *
     * <p>A helper method that processes getValues() and merges contents of
     * getPreferredLibraries() and getLinkOpts() into the current link params
     * object.
     */
    public Builder addCcLibrary(RuleContext context, boolean neverlink, List<String> linkopts,
        CcLinkingOutputs linkingOutputs) {
      addTransitiveTargets(
          context.getPrerequisites("deps", Mode.TARGET),
          CcLinkParamsProvider.TO_LINK_PARAMS, CcSpecificLinkParamsProvider.TO_LINK_PARAMS);

      if (!neverlink) {
        addLibraries(linkingOutputs.getPreferredLibraries(linkingStatically,
            linkShared || context.getFragment(CppConfiguration.class).forcePic()));
        addLinkOpts(linkopts);
      }
      return this;
    }
  }

  /**
   * A linkstamp that also knows about its declared includes.
   *
   * <p>This object is required because linkstamp files may include other headers which
   * will have to be provided during compilation.
   */
  public static final class Linkstamp {
    private final Artifact artifact;
    private final NestedSet<Artifact> declaredIncludeSrcs;

    private Linkstamp(Artifact artifact, NestedSet<Artifact> declaredIncludeSrcs) {
      this.artifact = Preconditions.checkNotNull(artifact);
      this.declaredIncludeSrcs = Preconditions.checkNotNull(declaredIncludeSrcs);
    }

    /**
     * Returns the linkstamp artifact.
     */
    public Artifact getArtifact() {
      return artifact;
    }

    /**
     * Returns the declared includes.
     */
    public NestedSet<Artifact> getDeclaredIncludeSrcs() {
      return declaredIncludeSrcs;
    }

    @Override
    public int hashCode() {
      return Objects.hash(artifact, declaredIncludeSrcs);
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (!(obj instanceof Linkstamp)) {
        return false;
      }
      Linkstamp other = (Linkstamp) obj;
      return artifact.equals(other.artifact)
          && declaredIncludeSrcs.equals(other.declaredIncludeSrcs);
    }
  }

  /**
   * Empty CcLinkParams.
   */
  public static final CcLinkParams EMPTY = new CcLinkParams(
      NestedSetBuilder.<ImmutableList<String>>emptySet(Order.LINK_ORDER),
      NestedSetBuilder.<Linkstamp>emptySet(Order.COMPILE_ORDER),
      NestedSetBuilder.<LibraryToLink>emptySet(Order.LINK_ORDER),
      null);
}
