// Copyright 2015 The Bazel Authors. 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
//
// 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.devtools.build.lib.profiler.statistics;

import com.google.devtools.build.lib.profiler.ProfileInfo.Task;
import com.google.devtools.build.lib.util.LongArrayList;
import java.util.List;

/**
 * Data container to aggregate execution time statistics of multiple tasks grouped by some name.
 */
public class TasksStatistics {
  public final String name;
  public final int count;
  public final long minNanos;
  public final long maxNanos;
  public final double medianNanos;
  /** Standard deviation of the execution time in milliseconds since computation in nanoseconds can
   * overflow.
   */
  public final double standardDeviationMillis;
  public final long totalNanos;

  public TasksStatistics(
      String name,
      int count,
      long minNanos,
      long maxNanos,
      double medianNanos,
      double standardDeviationMillis,
      long totalNanos) {
    this.name = name;
    this.count = count;
    this.minNanos = minNanos;
    this.maxNanos = maxNanos;
    this.medianNanos = medianNanos;
    this.standardDeviationMillis = standardDeviationMillis;
    this.totalNanos = totalNanos;
  }

  public double minimumMillis() {
    return toMilliSeconds(minNanos);
  }

  public double maximumMillis() {
    return toMilliSeconds(maxNanos);
  }

  public double meanNanos() {
    return totalNanos / count;
  }

  public double meanMillis() {
    return toMilliSeconds(meanNanos());
  }

  public double medianMillis() {
    return toMilliSeconds(medianNanos);
  }

  public double totalMillis() {
    return toMilliSeconds(totalNanos);
  }

  @Override
  public String toString() {
    return String.format(
        "%s %d %.3f %.3f %.3f %.3f %.3f %.3f",
        name,
        count,
        minimumMillis(),
        meanMillis(),
        medianMillis(),
        maximumMillis(),
        standardDeviationMillis,
        totalMillis());
  }

  /**
   * @return The set of statistics grouped in this class, computed from a list of {@link Task}s.
   */
  public static TasksStatistics create(String name, List<Task> tasks) {
    LongArrayList durations = new LongArrayList(tasks.size());
    for (Task task : tasks) {
      durations.add(task.durationNanos);
    }
    return create(name, durations);
  }

  /**
   * @return The set of statistics grouped in this class, computed from a list of durations
   */
  public static TasksStatistics create(String name, LongArrayList durations) {
    durations.sort();
    int count = durations.size();
    long min = durations.get(0);
    long max = durations.get(count - 1);

    int midIndex = count / 2;
    double median =
        count % 2 == 0
            ? (durations.get(midIndex) + durations.get(midIndex - 1)) / 2.0
            : durations.get(midIndex);

    // Compute standard deviation with a shift to avoid catastrophic cancellation
    // and also do it in milliseconds, as in nanoseconds it overflows
    long sum = 0L;
    double sumOfSquaredShiftedMillis = 0L;
    final long shift = min;

    for (int index = 0; index < count; index++) {
      sum += durations.get(index);
      double taskDurationShiftMillis = toMilliSeconds(durations.get(index) - shift);
      sumOfSquaredShiftedMillis += taskDurationShiftMillis * taskDurationShiftMillis;
    }
    double sumShiftedMillis = toMilliSeconds(sum - count * shift);

    double standardDeviation =
        Math.sqrt(
            (sumOfSquaredShiftedMillis - (sumShiftedMillis * sumShiftedMillis) / count) / count);

    return new TasksStatistics(name, count, min, max, median, standardDeviation, sum);
  }

  static double toMilliSeconds(double nanoseconds) {
    return nanoseconds / 1000000.0;
  }
}
