package hirondelle.web4j.util;

import java.math.BigDecimal;

/**
 Allows timing of the execution of any block of code.

 Example use case:
<PRE>
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
//..perform operations
stopwatch.stop();

//timed in nanos, but toString is expressed in millis,
//to three decimal places, to reflect the real resolution of most systems:
System.out.println("The reading on the stopwatch is: " + stopwatch);

//reuse the same stopwatch again
//Note that there is no need to call a reset method.
stopwatch.start();
//..perform operations
stopwatch.stop();

//perform a numeric comparison, using raw nanoseconds:
if ( stopwatch.toValue() > 5 ) {
  System.out.println("The reading is high: " + stopwatch);
}
</PRE>

 <P>The value on the stopwatch may be inspected at any time using the  
 {@link #toString} (millis) and {@link #toValue} (nanos) methods.
 
 <P><b>Example 2</b>
 <br>To time the various steps in a long startup or initialization task, your code may take the form:
<PRE>
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
//..perform operation 1
log("Step 1 completed " + stopwatch + " after start.");

//perform operation 2
log("Step 2 completed " + stopwatch + " after start.");

//perform the last operation, operation 3
stopwatch.stop();
log("Final Step 3 completed " + stopwatch + " after start") ;
</PRE>

<P><i>Implementation Note:</i></br>
 This class uses {@link System#nanoTime()}, not {@link System#currentTimeMillis()}, to 
 perform its timing operations.
*/
public final class Stopwatch {

  /**
   Start the stopwatch.
  
   <P>You cannot call this method if the stopwatch is already started.
  */
  public void start(){
    if (fIsRunning) {
      throw new IllegalStateException("Must stop before calling start again.");
    }
    //reset both start and stop
    fStart = getCurrentTime();
    fStop = 0;
    fIsRunning = true;
  }

  /**
   Stop the stopwatch.
  
   <P>You can only call this method if the stopwatch has been started.
  */
  public void stop() {
    if (!fIsRunning) {
      throw new IllegalStateException("Cannot stop if not currently running.");
    }
    fStop = getCurrentTime();
    fIsRunning = false;
  }

  /**
   Return the current "reading" on the stopwatch, in milliseconds, in a format suitable 
   typical use cases.
   
   <P>Example return value : '<tt>108.236 ms</tt>'. The underlying timing is in nanos, 
   but it's expressed here in millis, for two reasons: the resolution on most systems is 
   in the microsecond range, and the full 9 digits are less easy to reader. If you need 
   the raw nanos, just use {@link #toValue()} instead.
   
   <P>Ref: https://blogs.oracle.com/dholmes/entry/inside_the_hotspot_vm_clocks
  */
  @Override public String toString() {
    StringBuilder result = new StringBuilder();
    BigDecimal value = new BigDecimal(toValue());//scale is zero
    //millis, with 3 decimals:
    value = value.divide(MILLION, 3, BigDecimal.ROUND_HALF_EVEN);
    result.append(value);
    result.append(" ms");
    return result.toString();    
  }

  /**
   Return the current "reading" on the stopwatch in raw nanoseconds, as a numeric type.
  */
  public long toValue() {
    long result = fStop == 0 ? (getCurrentTime() - fStart) : (fStop -fStart);
    return result;
  }

  // PRIVATE 
  private long fStart;
  private long fStop;
  private boolean fIsRunning;
  
  /** Converts from nanos to millis. */
  private static final BigDecimal MILLION = new BigDecimal("1000000");
  
  private long getCurrentTime(){
    return System.nanoTime();
  }
}
