package io.takari.maven.builder.smart;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.project.MavenProject;

import com.google.common.collect.ImmutableMap;

class ReactorBuildStats {

  private long startTime;

  private long stopTime;

  /**
   * Time, in nanoseconds, a worker thread was executing the project build lifecycle. In addition to
   * Maven plugin goals execution includes any "overhead" time Maven spends resolving project
   * dependencies, calculating build time and perform any post-execution cleanup and maintenance.
   */
  private final Map<String, AtomicLong> serviceTimes;

  /**
   * Time, in nanoseconds, when the project was a bottleneck of entire build, i.e. when not all
   * available CPU cores were utilized, presumably because the project build time and dependency
   * structure prevented higher degree of parallelism.
   */
  private final Map<String, AtomicLong> bottleneckTimes;

  private ReactorBuildStats(Map<String, AtomicLong> serviceTimes,
      Map<String, AtomicLong> bottleneckTimes) {
    this.serviceTimes = ImmutableMap.copyOf(serviceTimes);
    this.bottleneckTimes = ImmutableMap.copyOf(bottleneckTimes);
  }

  private static String projectGA(MavenProject project) {
    return project.getGroupId() + ":" + project.getArtifactId();
  }

  public static ReactorBuildStats create(Collection<MavenProject> projects) {
    ImmutableMap.Builder<String, AtomicLong> serviceTimes = ImmutableMap.builder();
    ImmutableMap.Builder<String, AtomicLong> bottleneckTimes = ImmutableMap.builder();
    projects.stream().map(project -> projectGA(project)).forEach(key -> {
      serviceTimes.put(key, new AtomicLong());
      bottleneckTimes.put(key, new AtomicLong());
    });
    return new ReactorBuildStats(serviceTimes.build(), bottleneckTimes.build());
  }

  public void recordStart() {
    this.startTime = System.nanoTime();
  }

  public void recordStop() {
    this.stopTime = System.nanoTime();
  }

  public void recordServiceTime(MavenProject project, long durationNanos) {
    serviceTimes.get(projectGA(project)).addAndGet(durationNanos);
  }

  public void recordBottlenecks(Set<MavenProject> projects, int degreeOfConcurrency,
      long durationNanos) {
    // only projects that result in single-threaded builds
    if (projects.size() == 1) {
      projects.forEach(p -> bottleneckTimes.get(projectGA(p)).addAndGet(durationNanos));
    }
  }

  //
  // Reporting
  //

  public long totalServiceTime(TimeUnit unit) {
    long nanos = serviceTimes.values().stream().mapToLong(l -> l.longValue()).sum();
    return unit.convert(nanos, TimeUnit.NANOSECONDS);
  }

  public long walltimeTime(TimeUnit unit) {
    return unit.convert(stopTime - startTime, TimeUnit.NANOSECONDS);
  }

  public String renderCriticalPath(ProjectDependencyGraph graph) {
    return renderCriticalPath(DependencyGraph.fromMaven(graph), p -> projectGA(p));
  }

  public <K> String renderCriticalPath(DependencyGraph<K> graph, Function<K, String> toKey) {
    StringBuilder result = new StringBuilder();

    // render critical path

    long criticalPathServiceTime = 0;
    result.append("Build critical path service times (and bottleneck** times):");
    for (K project : calculateCriticalPath(graph, toKey)) {
      result.append('\n');
      String key = toKey.apply(project);
      criticalPathServiceTime += serviceTimes.get(key).get();
      appendProjectTimes(result, key);
    }
    result.append(String.format("\nBuild critical path total service time %s",
        formatDuration(criticalPathServiceTime)));

    // render bottleneck projects

    List<String> bottleneckProjects = getBottleneckProjects();
    if (!bottleneckProjects.isEmpty()) {
      long bottleneckTotalTime = 0;
      result.append("\nBuild bottleneck projects service times (and bottleneck** times):");
      for (String bottleneck : bottleneckProjects) {
        result.append('\n');
        bottleneckTotalTime += bottleneckTimes.get(bottleneck).get();
        appendProjectTimes(result, bottleneck);
      }
      result.append(
          String.format("\nBuild bottlenecks total time %s", formatDuration(bottleneckTotalTime)));
    }

    result.append("\n** Bottlenecks are projects that limit build concurrency");
    result.append("\n   removing bottlenecks improves overall build time");
    return result.toString();
  }

  private void appendProjectTimes(StringBuilder result, String project) {
    final long serviceTime = serviceTimes.get(project).get();
    final long bottleneckTime = bottleneckTimes.get(project).get();
    result.append(String.format("   %-60s %s", project, formatDuration(serviceTime)));
    if (bottleneckTime > 0) {
      result.append(String.format(" (%s)", formatDuration(bottleneckTime)));
    }
  }

  private List<String> getBottleneckProjects() {
    Comparator<String> comparator = (a, b) -> {
      long ta = bottleneckTimes.get(a).longValue();
      long tb = bottleneckTimes.get(b).longValue();
      if (tb > ta) {
        return 1;
      } else if (tb < ta) {
        return -1;
      }
      return 0;
    };
    return bottleneckTimes.keySet().stream() //
        .sorted(comparator) //
        .filter(project -> bottleneckTimes.get(project).get() > 0) //
        .collect(Collectors.toList());
  }

  private String formatDuration(long nanos) {
    long secs = TimeUnit.NANOSECONDS.toSeconds(nanos);
    return String.format("%5d s", secs);
  }

  private <K> List<K> calculateCriticalPath(DependencyGraph<K> graph, Function<K, String> toKey) {
    Comparator<K> comparator = ProjectComparator.create0(graph, serviceTimes, toKey);
    List<K> rootProjects = new ArrayList<>();
    for (K project : graph.getSortedProjects()) {
      if (graph.getUpstreamProjects(project).isEmpty()) {
        rootProjects.add(project);
      }
    }
    List<K> criticalPath = new ArrayList<>();
    K project = getCriticalProject(rootProjects, comparator);
    do {
      criticalPath.add(project);
    } while ((project =
        getCriticalProject(graph.getDownstreamProjects(project), comparator)) != null);
    return criticalPath;
  }

  private <K> K getCriticalProject(Collection<K> projects, Comparator<K> comparator) {
    if (projects == null || projects.isEmpty()) {
      return null;
    }
    List<K> sorted = new ArrayList<>(projects);
    Collections.sort(sorted, comparator);
    return sorted.get(0);
  }

}