/*
 * 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.versions;

import com.facebook.buck.core.cell.Cells;
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.targetgraph.TargetGraphCreationResult;
import com.facebook.buck.core.model.targetgraph.TargetNode;
import com.facebook.buck.core.model.targetgraph.impl.TargetNodes;
import com.facebook.buck.core.parser.buildtargetparser.UnconfiguredBuildTargetViewFactory;
import com.facebook.buck.rules.coercer.TypeCoercerFactory;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

public abstract class AbstractVersionedTargetGraphBuilder implements VersionedTargetGraphBuilder {

  protected final TypeCoercerFactory typeCoercerFactory;
  private final UnconfiguredBuildTargetViewFactory unconfiguredBuildTargetFactory;
  protected final TargetGraphCreationResult unversionedTargetGraphCreationResult;
  protected final long timeout;
  protected final TimeUnit timeUnit;
  private final Cells cells;

  protected AbstractVersionedTargetGraphBuilder(
      TypeCoercerFactory typeCoercerFactory,
      UnconfiguredBuildTargetViewFactory unconfiguredBuildTargetFactory,
      TargetGraphCreationResult unversionedTargetGraphCreationResult,
      long timeout,
      TimeUnit timeUnit,
      Cells cells) {

    this.typeCoercerFactory = typeCoercerFactory;
    this.unconfiguredBuildTargetFactory = unconfiguredBuildTargetFactory;
    this.unversionedTargetGraphCreationResult = unversionedTargetGraphCreationResult;
    this.timeout = timeout;
    this.timeUnit = timeUnit;
    this.cells = cells;
  }

  /** @return a flavor to which summarizes the given version selections. */
  static Flavor getVersionedFlavor(SortedMap<BuildTarget, Version> versions) {
    Preconditions.checkArgument(!versions.isEmpty());
    Hasher hasher = Hashing.md5().newHasher();
    for (Map.Entry<BuildTarget, Version> ent : versions.entrySet()) {
      hasher.putString(ent.getKey().toString(), Charsets.UTF_8);
      hasher.putString(ent.getValue().getName(), Charsets.UTF_8);
    }
    return InternalFlavor.of("v" + hasher.hash().toString().substring(0, 7));
  }

  protected TargetNode<?> getNode(BuildTarget target) {
    return unversionedTargetGraphCreationResult.getTargetGraph().get(target);
  }

  protected Optional<TargetNode<?>> getNodeOptional(BuildTarget target) {
    return unversionedTargetGraphCreationResult.getTargetGraph().getOptional(target);
  }

  protected final TargetNode<?> resolveVersions(
      TargetNode<?> node, ImmutableMap<BuildTarget, Version> selectedVersions) {
    Optional<TargetNode<VersionedAliasDescriptionArg>> versionedNode =
        TargetNodes.castArg(node, VersionedAliasDescriptionArg.class);
    if (versionedNode.isPresent()) {
      node =
          getNode(
              Preconditions.checkNotNull(
                  versionedNode
                      .get()
                      .getConstructorArg()
                      .getVersions()
                      .get(selectedVersions.get(node.getBuildTarget()))));
    }
    return node;
  }

  /** Get/cache the transitive version info for this node. */
  protected abstract VersionInfo getVersionInfo(TargetNode<?> node);

  /**
   * @return the {@link BuildTarget} to use in the resolved target graph, formed by adding a flavor
   *     generated from the given version selections.
   */
  protected final Optional<BuildTarget> getTranslateBuildTarget(
      TargetNode<?> node, ImmutableMap<BuildTarget, Version> selectedVersions) {

    BuildTarget originalTarget = node.getBuildTarget();
    node = resolveVersions(node, selectedVersions);
    BuildTarget newTarget = node.getBuildTarget();

    if (TargetGraphVersionTransformations.isVersionPropagator(node)) {
      VersionInfo info = getVersionInfo(node);
      Collection<BuildTarget> versionedDeps = info.getVersionDomain().keySet();
      TreeMap<BuildTarget, Version> versions = new TreeMap<>();
      for (BuildTarget depTarget : versionedDeps) {
        versions.put(depTarget, selectedVersions.get(depTarget));
      }
      if (!versions.isEmpty()) {
        Flavor versionedFlavor = getVersionedFlavor(versions);
        newTarget = node.getBuildTarget().withAppendedFlavors(versionedFlavor);
      }
    }

    return newTarget.equals(originalTarget) ? Optional.empty() : Optional.of(newTarget);
  }

  protected TargetNodeTranslator getTargetNodeTranslator(
      TargetNode<?> root, ImmutableMap<BuildTarget, Version> selectedVersions) {
    // Build a target translator object to translate build targets.
    ImmutableList<TargetTranslator<?>> translators =
        ImmutableList.of(new QueryTargetTranslator(unconfiguredBuildTargetFactory));
    return new TargetNodeTranslator(typeCoercerFactory, translators, cells) {

      private final LoadingCache<BuildTarget, Optional<BuildTarget>> cache =
          CacheBuilder.newBuilder()
              .build(
                  CacheLoader.from(
                      target -> {

                        // If we're handling the root node, there's nothing to translate.
                        if (root.getBuildTarget().equals(target)) {
                          return Optional.empty();
                        }

                        // If this target isn't in the target graph, which can be the case
                        // of build targets in the `tests` parameter, don't do any
                        // translation.
                        Optional<TargetNode<?>> node = getNodeOptional(target);
                        if (!node.isPresent()) {
                          return Optional.empty();
                        }

                        return getTranslateBuildTarget(getNode(target), selectedVersions);
                      }));

      @Override
      public Optional<BuildTarget> translateBuildTarget(BuildTarget target) {
        return cache.getUnchecked(target);
      }

      @Override
      public Optional<ImmutableMap<BuildTarget, Version>> getSelectedVersions(BuildTarget target) {
        ImmutableMap.Builder<BuildTarget, Version> builder = ImmutableMap.builder();
        for (BuildTarget dep : getVersionInfo(getNode(target)).getVersionDomain().keySet()) {
          builder.put(dep, selectedVersions.get(dep));
        }
        return Optional.of(builder.build());
      }
    };
  }
}
