/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.function.benchmark;

import nz.org.riskscape.engine.RiskscapeException;

import java.util.List;

import org.apache.commons.math3.stat.descriptive.moment.Mean;

import com.google.common.collect.Lists;
import java.time.Duration;

/**
 * Timer to get the mean duration over a series of events.
 *
 * The {@link #start()} and {@link #stop()} methods should wrap the event that is to be timed.
 */
public class Timer {

  private static final int ONE_THOUSAND = 1000;
  private final String name;
  private int count;
  private Mean mean;
  private long start;
  private Duration total;

  public Timer(String name) {
    this.name = name;
    reset();
  }

  /**
   * Start the timer.
   */
  public void start() {
    if (start != 0) {
      throw new RiskscapeException(String.format("Cannot start timer '%s', already running", name));
    }
    start = System.nanoTime();
  }

  /**
   * Stop the timer.
   */
  public void stop() throws ArithmeticException {
    if (start == 0) {
      throw new RiskscapeException(String.format("Cannot stop timer '%s', not running", name));
    }
    ++count;
    long duration = System.nanoTime() - start;
    total = total.plusNanos(duration);
    mean.increment(duration);
    start = 0;
  }

  /**
   * Reset the timer to initial state.
   */
  public final void reset() {
    count = 0;
    mean = new Mean();
    start = 0;
    total = Duration.ZERO;
  }

  private static final String STATS_FORMAT = "%s%n"
      + "  Iterations      : %s%n"
      + "  Round Time      : %s%n"
      + "  Mean            : %s%n";

  static String formatTime(double nanoTime) {
    double timeValue = nanoTime;
    List<String> units = Lists.newArrayList("ns", "μs", "ms", "s");

    int unit = 0;
    while (timeValue >= ONE_THOUSAND && unit < units.size() - 1) {
      timeValue = timeValue / ONE_THOUSAND;
      unit++;
    }

    return String.format("%7.2f %s", timeValue, units.get(unit));

  }

  static String formatTime(Duration d) {
    if (d.getSeconds() > 0) {
      float dSec = d.getSeconds() + (1.0F * d.getNano() / (ONE_THOUSAND * ONE_THOUSAND * ONE_THOUSAND));
      return String.format("%7.2f s", dSec);
    }
    return formatTime(d.getNano());
  }

  /**
   * @return current stats of this timer
   */
  public String getStats() {
    if (count == 0) {
      return String.format("Timer '%s' has no measurements", name);
    }
    return String.format(STATS_FORMAT,
        name,
        count,
        formatTime(total),
        formatTime(mean.getResult())
        );
  }

}
