/*
 * Copyright (c) 2014 - 2019 the original author or authors.
 *
 * 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.github.ferstl.depgraph.dependency;

import java.util.Set;
import java.util.TreeSet;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.eclipse.aether.util.graph.transformer.ConflictResolver;
import com.google.common.collect.ImmutableSet;
import static com.github.ferstl.depgraph.dependency.NodeResolution.INCLUDED;
import static com.github.ferstl.depgraph.dependency.NodeResolution.OMITTED_FOR_CONFLICT;
import static com.github.ferstl.depgraph.dependency.NodeResolution.OMITTED_FOR_DUPLICATE;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE;

/**
 * Representation of a dependency graph node. It adapts these Maven-specific classes:
 * <ul>
 * <li>{@link org.apache.maven.artifact.Artifact}</li>
 * <li>{@link org.eclipse.aether.graph.DependencyNode}</li>
 * </ul>
 */
public final class DependencyNode {

  private final Artifact artifact;
  private final String effectiveVersion;
  private final NodeResolution resolution;
  private final Set<String> scopes;
  private final Set<String> classifiers;
  private final Set<String> types;


  public DependencyNode(Artifact artifact) {
    this(artifact, determineNodeResolution(artifact), artifact.getVersion());
  }

  public DependencyNode(org.eclipse.aether.graph.DependencyNode dependencyNode) {
    this(createMavenArtifact(dependencyNode), determineResolution(dependencyNode), determineEffectiveVersion(dependencyNode));
  }

  private DependencyNode(Artifact artifact, NodeResolution resolution, String effectiveVersion) {
    if (artifact == null) {
      throw new NullPointerException("Artifact must not be null");
    }

    this.effectiveVersion = effectiveVersion;
    this.scopes = new TreeSet<>();
    this.classifiers = new TreeSet<>();
    this.types = new TreeSet<>();
    this.artifact = artifact;
    this.resolution = resolution;
    if (artifact.getScope() != null) {
      this.scopes.add(artifact.getScope());
    }
    this.types.add(artifact.getType());

    if (!isNullOrEmpty(artifact.getClassifier())) {
      this.classifiers.add(artifact.getClassifier());
    }
  }

  public void merge(DependencyNode other) {
    if (this == other) {
      return;
    }

    if (this.artifact.isOptional()) {
      this.artifact.setOptional(other.getArtifact().isOptional());
    }
    this.scopes.addAll(other.scopes);
    this.classifiers.addAll(other.classifiers);
    this.types.addAll(other.types);
  }

  public Artifact getArtifact() {
    return this.artifact;
  }

  public NodeResolution getResolution() {
    return this.resolution;
  }

  public Set<String> getScopes() {
    return ImmutableSet.copyOf(this.scopes);
  }

  public Set<String> getClassifiers() {
    return ImmutableSet.copyOf(this.classifiers);
  }

  public Set<String> getTypes() {
    return ImmutableSet.copyOf(this.types);
  }


  /**
   * Returns the <strong>effective</strong> version of this node, i.e. the version that is actually used. This is
   * important for nodes with a resolution of {@link NodeResolution#OMITTED_FOR_CONFLICT} where
   * {@code getArtifact().getVersion()} will return the omitted version.
   *
   * @return The effective version of this node.
   */
  public String getEffectiveVersion() {
    return this.effectiveVersion;
  }

  /**
   * Returns the <strong>effective</strong> scope of this node, i.e. the scope that is actually used. This is important
   * if scopes are merged and a node may have more than one scope.
   *
   * @return The effective scope of this node.
   */
  public String getEffectiveScope() {
    if (this.scopes.size() > 0) {
      return this.scopes.iterator().next();
    }

    return SCOPE_COMPILE;
  }


  @Override
  public String toString() {
    return this.artifact.toString();
  }

  private static Artifact createMavenArtifact(org.eclipse.aether.graph.DependencyNode dependencyNode) {
    org.eclipse.aether.artifact.Artifact artifact = dependencyNode.getArtifact();
    String scope = null;
    boolean optional = false;
    if (dependencyNode.getDependency() != null) {
      scope = dependencyNode.getDependency().getScope();
      optional = dependencyNode.getDependency().isOptional();
    }

    DefaultArtifact mavenArtifact = new DefaultArtifact(
        artifact.getGroupId(),
        artifact.getArtifactId(),
        artifact.getVersion(),
        scope,
        artifact.getProperty("type", artifact.getExtension()),
        artifact.getClassifier(),
        null
    );
    mavenArtifact.setOptional(optional);

    return mavenArtifact;
  }

  private static NodeResolution determineResolution(org.eclipse.aether.graph.DependencyNode dependencyNode) {
    org.eclipse.aether.graph.DependencyNode winner = (org.eclipse.aether.graph.DependencyNode) dependencyNode.getData().get(ConflictResolver.NODE_DATA_WINNER);

    if (winner != null) {
      if (winner.getArtifact().getVersion().equals(dependencyNode.getArtifact().getVersion())) {
        return OMITTED_FOR_DUPLICATE;
      }

      return OMITTED_FOR_CONFLICT;
    }

    return INCLUDED;
  }

  private static NodeResolution determineNodeResolution(Artifact artifact) {
    if (artifact.getScope() == null) {
      return NodeResolution.PARENT;
    }

    return NodeResolution.INCLUDED;
  }

  private static String determineEffectiveVersion(org.eclipse.aether.graph.DependencyNode dependencyNode) {
    org.eclipse.aether.graph.DependencyNode winner = (org.eclipse.aether.graph.DependencyNode) dependencyNode.getData().get(ConflictResolver.NODE_DATA_WINNER);
    if (winner != null) {
      return winner.getArtifact().getVersion();
    }

    return dependencyNode.getArtifact().getVersion();
  }
}