/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.maven.lifecycle.internal.concurrent;

import javax.inject.Named;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * Logs debug output from the various lifecycle phases.
 * </p>
 * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
 *
 * @since 3.0
 */
@Named
public class BuildPlanLogger {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    public void writePlan(BuildPlan plan) {
        if (logger.isDebugEnabled()) {
            writePlan(logger::debug, plan);
        }
    }

    public void writePlan(BuildPlan plan, MavenProject project) {
        if (logger.isDebugEnabled()) {
            writePlan(logger::debug, plan, project);
        }
    }

    public void writePlan(Consumer<String> writer, BuildPlan plan) {
        plan.projects().forEach(project -> writePlan(writer, plan, project));
    }

    public void writePlan(Consumer<String> writer, BuildPlan plan, MavenProject project) {
        writer.accept("=== PROJECT BUILD PLAN ================================================");
        writer.accept("Project:                     " + getKey(project));
        writer.accept("Repositories (dependencies): " + project.getRemoteProjectRepositories());
        writer.accept("Repositories (plugins):      " + project.getRemotePluginRepositories());

        Optional<BuildStep> planStep = plan.step(project, BuildStep.PLAN);
        if (planStep.isPresent() && planStep.get().status.get() == BuildStep.PLANNING) {
            writer.accept("Build plan will be lazily computed");
        } else {
            plan.steps(project)
                    .filter(step ->
                            step.phase != null && step.executions().findAny().isPresent())
                    .sorted(Comparator.comparingInt(plan.sortedNodes()::indexOf))
                    .forEach(step -> {
                        writer.accept("\t-----------------------------------------------------------------------");
                        writer.accept("\tPhase:         " + step.name);
                        if (!step.predecessors.isEmpty()) {
                            writer.accept("\tPredecessors:  "
                                    + nonEmptyPredecessors(step)
                                            .map(n -> phase(project, n, plan.duplicateIds()))
                                            .collect(Collectors.joining(", ")));
                        }
                        step.mojos.values().stream()
                                .flatMap(m -> m.values().stream())
                                .forEach(mojo -> mojo(writer, mojo));
                    });
        }

        writer.accept("=======================================================================");
    }

    protected Stream<BuildStep> nonEmptyPredecessors(BuildStep step) {
        HashSet<BuildStep> preds = new HashSet<>();
        nonEmptyPredecessors(step, preds, new HashSet<>());
        return preds.stream();
    }

    private void nonEmptyPredecessors(BuildStep step, Set<BuildStep> preds, Set<BuildStep> visited) {
        if (visited.add(step)) {
            step.predecessors.forEach(ch -> {
                if (ch.executions().findAny().isPresent()) {
                    preds.add(ch);
                } else {
                    nonEmptyPredecessors(ch, preds, visited);
                }
            });
        }
    }

    protected String phase(MavenProject currentProject, BuildStep step, Set<String> duplicateIds) {
        if (step.project == currentProject) {
            return step.name;
        } else {
            String artifactId = step.project.getArtifactId();
            if (duplicateIds.contains(artifactId)) {
                return step.name + "(" + step.project.getGroupId() + ":" + artifactId + ")";
            } else {
                return step.name + "(:" + artifactId + ")";
            }
        }
    }

    protected void mojo(Consumer<String> writer, MojoExecution mojoExecution) {
        String mojoExecId =
                mojoExecution.getGroupId() + ':' + mojoExecution.getArtifactId() + ':' + mojoExecution.getVersion()
                        + ':' + mojoExecution.getGoal() + " (" + mojoExecution.getExecutionId() + ')';

        Map<String, List<MojoExecution>> forkedExecutions = mojoExecution.getForkedExecutions();
        if (!forkedExecutions.isEmpty()) {
            for (Map.Entry<String, List<MojoExecution>> fork : forkedExecutions.entrySet()) {
                writer.accept("\t--- init fork of " + fork.getKey() + " for " + mojoExecId + " ---");

                for (MojoExecution forkedExecution : fork.getValue()) {
                    mojo(writer, forkedExecution);
                }

                writer.accept("\t--- exit fork of " + fork.getKey() + " for " + mojoExecId + " ---");
            }
        }

        writer.accept("\t\t-----------------------------------------------------------------------");
        if (mojoExecution.getMojoDescriptor().isAggregator()) {
            writer.accept("\t\tAggregator goal:        " + mojoExecId);
        } else {
            writer.accept("\t\tGoal:                   " + mojoExecId);
        }
        if (mojoExecution.getConfiguration() != null) {
            writer.accept("\t\tConfiguration:          " + mojoExecution.getConfiguration());
        }
        if (mojoExecution.getMojoDescriptor().getDependencyCollectionRequired() != null) {
            writer.accept("\t\tDependencies (collect): "
                    + mojoExecution.getMojoDescriptor().getDependencyCollectionRequired());
        }
        if (mojoExecution.getMojoDescriptor().getDependencyResolutionRequired() != null) {
            writer.accept("\t\tDependencies (resolve): "
                    + mojoExecution.getMojoDescriptor().getDependencyResolutionRequired());
        }
    }

    protected String getKey(MavenProject project) {
        return project.getGroupId() + ':' + project.getArtifactId() + ':' + project.getVersion();
    }
}
