/*
 * Copyright 2020 Google LLC.
 *
 * 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.cloud.tools.opensource.dependencies;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Set;
import org.eclipse.aether.RepositoryException;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.collection.DependencyGraphTransformationContext;
import org.eclipse.aether.collection.DependencyGraphTransformer;
import org.eclipse.aether.graph.DependencyNode;

/**
 * Transforms a dependency graph so that it will not contain cycles.
 *
 * <p>A cycle in a dependency graph is a situation where a path to a node from the root contains the
 * same node. For example, jaxen 1.1-beta-6 is known to have cycle with dom4j 1.6.1.
 */
final class CycleBreakerGraphTransformer implements DependencyGraphTransformer {

  private final Set<DependencyNode> visitedNodes =
      Collections.newSetFromMap(new IdentityHashMap<>());

  @Override
  public DependencyNode transformGraph(
      DependencyNode dependencyNode, DependencyGraphTransformationContext context)
      throws RepositoryException {

    removeCycle(null, dependencyNode, new HashSet<>());
    return dependencyNode;
  }

  private void removeCycle(DependencyNode parent, DependencyNode node, Set<Artifact> ancestors) {
    Artifact artifact = node.getArtifact();

    if (ancestors.contains(artifact)) { // Set (rather than List) gives O(1) lookup here
      // parent is not null when ancestors is not empty
      removeChildFromParent(node, parent);
      return;
    }

    if (shouldVisitChildren(node)) {
      ancestors.add(artifact);
      for (DependencyNode child : node.getChildren()) {
        removeCycle(node, child, ancestors);
      }
      ancestors.remove(artifact);
    }
  }

  /** Returns true if {@code node} is not visited yet and marks the node as visited. */
  @VisibleForTesting
  boolean shouldVisitChildren(DependencyNode node) {
    return visitedNodes.add(node);
  }

  private static void removeChildFromParent(DependencyNode child, DependencyNode parent) {
    ImmutableList<DependencyNode> children =
        parent.getChildren().stream()
            .filter(node -> node != child)
            .collect(ImmutableList.toImmutableList());
    parent.setChildren(children);
  }
}