/*
 * Copyright 2021, TeamDev. 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
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.tools.mc.java.gradle;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.spine.logging.Logging;
import io.spine.tools.gradle.DependencyVersions;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.artifacts.ModuleIdentifier;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.result.DependencyResult;
import org.gradle.api.artifacts.result.ResolutionResult;
import org.gradle.api.artifacts.result.ResolvedComponentResult;
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
import org.gradle.api.artifacts.result.UnresolvedDependencyResult;
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency;

import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static io.spine.tools.gradle.Artifact.SPINE_TOOLS_GROUP;
import static java.lang.String.format;

/**
 * Adds a {@code spine-mc-java-checks} dependency to the given project {@link Configuration}.
 */
public final class McJavaChecksDependency implements Logging {

    /**
     * The name of the Maven artifact of the Model Compiler Java Checks.
     */
    @VisibleForTesting
    public static final String SPINE_MC_JAVA_CHECKS_ARTIFACT = "spine-mc-java-checks";

    /** The configuration to be extended. */
    private final Configuration configuration;

    /** The version of the dependency, which is the same as one for {@code spine-base}. */
    private final String version;

    private McJavaChecksDependency(Configuration cfg) {
        this.configuration = cfg;
        DependencyVersions versions = DependencyVersions.get();
        this.version = versions.spineBase();
    }

    /**
     * Adds the dependency of the Spine Model Checks to the given configuration.
     *
     * @param project
     *         the project to which apply the dependency
     * @return true if the configuration was applied
     */
    public static boolean addTo(Project project) {
        checkNotNull(project);
        Configuration cfg = AnnotationProcessorConfiguration.findOrCreateIn(project);
        McJavaChecksDependency dep = new McJavaChecksDependency(cfg);
        boolean result = dep.addDependency();
        return result;
    }

    /**
     * Adds the dependency to the project configuration.
     *
     * @return {@code true} if the operation was successful, {@code false} otherwise
     */
    private boolean addDependency() {
        ResolutionHelper helper = new ResolutionHelper();
        if (helper.wasResolved()) {
            addDependencyTo(configuration);
            return true;
        } else {
            helper.logUnresolved();
            return false;
        }
    }

    /**
     * Adds the dependency to the project configuration.
     */
    private void addDependencyTo(Configuration cfg) {
        _debug().log("Adding a dependency on `%s` to the `%s` configuration.", artifactId(), cfg);
        DependencySet dependencies = cfg.getDependencies();
        Dependency dependency = checksDependency();
        dependencies.add(dependency);
    }

    private String artifactId() {
        return format("%s:%s:%s", SPINE_TOOLS_GROUP, SPINE_MC_JAVA_CHECKS_ARTIFACT, version);
    }

    private DefaultExternalModuleDependency checksDependency() {
        return new DefaultExternalModuleDependency(
                SPINE_TOOLS_GROUP, SPINE_MC_JAVA_CHECKS_ARTIFACT, version
        );
    }

    /**
     * Assists with checking if the dependency can be resolved, and if not, helps with
     * logging error diagnostics.
     */
    private class ResolutionHelper {

        private final ResolutionResult resolutionResult;
        private final ModuleIdentifier checksModule;

        private ResolutionHelper() {
            Configuration configCopy = configuration.copy();
            addDependencyTo(configCopy);
            resolutionResult = configCopy.getIncoming()
                                         .getResolutionResult();
            checksModule = checksDependency().getModule();
        }

        private boolean wasResolved() {
            boolean wasResolved =
                    allDeps().stream()
                             .filter(ResolvedDependencyResult.class::isInstance)
                             .map(DependencyResult::getFrom)
                             .map(ResolvedComponentResult::getModuleVersion)
                             .filter(Objects::nonNull)
                             .map(ModuleVersionIdentifier::getModule)
                             .anyMatch(checksModule::equals);
            return wasResolved;
        }

        private Set<? extends DependencyResult> allDeps() {
            return resolutionResult.getAllDependencies();
        }

        private ImmutableList<UnresolvedDependencyResult> unresolved() {
            return allDeps().stream()
                            .filter(UnresolvedDependencyResult.class::isInstance)
                            .map(UnresolvedDependencyResult.class::cast)
                            .collect(toImmutableList());
        }

        private void logUnresolved() {
            ImmutableMap<String, Throwable> diags =
                    unresolved().stream()
                                .collect(toImmutableMap(
                                        this::resultDisplayName,
                                        UnresolvedDependencyResult::getFailure
                                ));
            ImmutableList<String> problemReport =
                    diags.entrySet()
                         .stream()
                         .map(this::toErrorMessage)
                         .sorted()
                         .collect(toImmutableList());
            _warn().log(
                    "Unable to add a dependency on `%s` to the configuration `%s` because some " +
                            "dependencies could not be resolved: " +
                            "%s.",
                    artifactId(), configuration.getName(), problemReport
            );
        }

        private String resultDisplayName(UnresolvedDependencyResult r) {
            return r.getAttempted()
                    .getDisplayName();
        }

        private String toErrorMessage(Map.Entry<String, Throwable> entry) {
            String dependency = entry.getKey();
            Throwable throwable = entry.getValue();
            return format("%nDependency: `%s`%nProblem: `%s`", dependency, throwable);
        }
    }
}

@@ -5328,61 +5328,8 @@
 ult;
-%0A        private final ModuleIdentifier checksModule;
 %0A%0A  
@@ -5590,67 +5590,8 @@
 ();%0A
-            checksModule = checksDependency().getModule();%0A
     
@@ -5674,613 +5674,63 @@
 ed =
-%0A                    allDeps().stream()%0A                             .filter(ResolvedDependencyResult.class::isInstance)%0A                             .map(DependencyResult::getFrom)%0A                             .map(ResolvedComponentResult::getModuleVersion)%0A                             .filter(Objects::nonNull)%0A                             .map(ModuleVersionIdentifier::getModule)%0A                             .anyMatch(checksModule::equals);%0A            return wasResolved;%0A        %7D%0A%0A        private Set%3C? extends DependencyResult%3E allDeps() %7B%0A            return resolutionResult.getAllDependencies()
+ unresolved().isEmpty();%0A            return wasResolved
 ;%0A  
@@ -6064,32 +6064,161 @@
 ());%0A        %7D%0A%0A
+        private Set%3C? extends DependencyResult%3E allDeps() %7B%0A            return resolutionResult.getAllDependencies();%0A        %7D%0A%0A
         private 

Simplify checking unresolved dependencies