package hirondelle.web4j.webmaster;

import java.util.*;
import java.util.logging.*;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.Util;

/**
 Statistics on server response time.
 
 <P>This class uses the metaphor of a 'photographic exposure' of, say, 10 minutes, whereby response times
 for a specific time interval are grouped together in a single bin, and average and maximum response times for 
 that interval are derived for that group.
 
 <P>A particular <tt>PerformanceSnapshot</tt> is used only if its 'exposure time' 
 has not yet ended. A typical 'exposure' lasts a few minutes. 
 (See {@link hirondelle.web4j.webmaster.PerformanceMonitor} and the <tt>web.xml</tt> 
 of the example application for more information.) 
 By inspecting the return value of {@link #getEndTime}, <em>the caller 
 determines if a <tt>PerformanceSnapshot</tt> object can still be used</em>, or if a new 
 <tt>PerformanceSnapshot</tt> object must be created for the 'next exposure'.

 <P>This class is immutable. In particular, {@link #addResponseTime} returns a new object, 
 instead of changing the state of an existing one.
*/
public final class PerformanceSnapshot {
  
  /**
   @param aExposureTime number of minutes to gather statistics ; see <tt>web.xml</tt>
   for more information.
  */
  public PerformanceSnapshot(Integer aExposureTime){
    fMaxUrl = Consts.EMPTY_STRING;
    fAvgResponseTime = 0L;
    fMaxResponseTime = 0L;
    fNumRequests = 0;
    fExposureTime = aExposureTime.intValue();
    fEndTime = calcEndTime();
  }

  /**
   Return a <tt>PerformanceSnapshot</tt> having no activity. 
   Such objects are used to explicitly 'fill in the gaps' during periods of no activity.
   
   <P>The returned object has the same exposure time as <tt>aCurrentSnapshot</tt>.
   Its end time is taken as <tt>aCurrentSnapshot.getEndTime()</tt>, plus the exposure time.
   All other items are <tt>0</tt> or empty. 
  */
  public static PerformanceSnapshot forGapInActivity(PerformanceSnapshot aCurrentSnapshot){
    return new PerformanceSnapshot(
      aCurrentSnapshot.getEndTime().getTime() + aCurrentSnapshot.getExposureTime()*60*1000, 
      Consts.EMPTY_STRING, 
      0L, 
      0L, 
      0,
      aCurrentSnapshot.getExposureTime()
    );
  }

  /**
   Return a new <tt>PerformanceSnapshot</tt> whose state reflects an additional  
   data point.
  
   @param aResponseTime response time of a particular server request.
   @param aURL URL of the underlying request.
  */
  public PerformanceSnapshot addResponseTime(long aResponseTime, String aURL){
    long maxResponseTime = 
      aResponseTime > fMaxResponseTime ? aResponseTime : fMaxResponseTime
    ;
    String maxUrl = aResponseTime > fMaxResponseTime ? aURL : fMaxUrl;
    int numRequests = fNumRequests + 1;
    return new PerformanceSnapshot(
      fEndTime, 
      maxUrl, 
      getNewAvgResponseTime(aResponseTime), 
      maxResponseTime, 
      numRequests,
      fExposureTime
    );
  }

  /**
   Return the time that this snapshot will 'end'. After this time, 
   a new <tt>PerformanceSnapshot</tt> must be created by the caller (using the  
   constructor).
  */
  public Date getEndTime(){
    return new Date(fEndTime);
  }
  
  /**
   Return the number of server requests this snapshot has recorded. 
  
   <P>If a page contains two images, for example, the server will likely count 
   3 requests, not 1 (one page and two images). 
  */
  public Integer getNumRequests(){
    return new Integer(fNumRequests);
  }
  
  /** Return the average response time recorded during this snapshot.  */
  public Long getAvgResponseTime(){
    return new Long(fAvgResponseTime);
  }
  
  /** Return the maximum response time recorded during this snapshot.  */
  public Long getMaxResponseTime(){
    return new Long(fMaxResponseTime);
  }
  
  /** Return the exposure time in minutes, as passed to the constructor.  */
  public Integer getExposureTime(){
    return new Integer(fExposureTime);
  }
  
  /** Return the URL of the request responsible for {@link #getMaxResponseTime}.  */
  public String getURLWithMaxResponseTime(){
    return fMaxUrl;
  }
  
  /** Intended for debugging only.  */
  @Override public String toString(){
    return ModelUtil.toStringFor(this);
  }
  
  @Override public boolean equals(Object aThat){
    if ( this == aThat ) return true;
    if ( !(aThat instanceof PerformanceSnapshot) ) return false;
    PerformanceSnapshot that = (PerformanceSnapshot)aThat;
    return ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
  }
  
  @Override public int hashCode(){
    return ModelUtil.hashCodeFor(getSignificantFields());
  }
  
  // PRIVATE //
  private final int fNumRequests;
  private final long fEndTime;
  private final long fAvgResponseTime;
  private final long fMaxResponseTime;
  private final int fExposureTime;
  private final String fMaxUrl;
  
  private static final Logger fLogger = Util.getLogger(PerformanceSnapshot.class);  
  
  private PerformanceSnapshot(
    long aEndTime, 
    String aMaxUrl, 
    long aAvgResponseTime, 
    long aMaxResponseTime, 
    int aNumRequests,
    int aExposureTime
  ){
    fEndTime = aEndTime;
    fMaxUrl = aMaxUrl;
    fAvgResponseTime = aAvgResponseTime;
    fMaxResponseTime = aMaxResponseTime;
    fNumRequests = aNumRequests;
    fExposureTime = aExposureTime;
  }
  
  /**
   Return a new average response time, rounded to the nearest millisecond.
  */
  private long getNewAvgResponseTime(long aNewResponseTime){
    //use integer division to do the rounding
    //here, all previous requests are treated as having the same average response time
    long numerator = (fAvgResponseTime * fNumRequests) + aNewResponseTime;
    long denominator = fNumRequests + 1;
    return numerator/denominator;
  }
  
  /**
   Return the time this snapshot's exposure will end.
  
   <P>The end time is the next 'round' minute of the hour in agreement with the 
   configured exposure time. For example, if the exposure time is 20 minutes, and 
   the current time is 32 minutes past the hour, the end time will be 40 minutes past 
   the hour. (If the current time is 40 minutes past the hour, the end time will be 
   60 minutes past the hour.)
  */
  private long calcEndTime(){
    //this item is used as a 'workspace', and its state is changed to find the 
    //desired end point :
    final Calendar result = new GregorianCalendar();
    //leniency will increment hour, day, and so on, if necessary :
    result.setLenient(true);
    result.setTimeInMillis(System.currentTimeMillis());
    //these items are not relevant to the result, since we need to return an even minute
    result.set(Calendar.MILLISECOND, 0);
    result.set(Calendar.SECOND, 0);
    fLogger.finest("Initial calendar, minus seconds : " + result.toString());
    //increase the minutes until the desired end point is reached
    //note this item in non-final
    int minute = result.get(Calendar.MINUTE); 
    fLogger.finest("Initial minute: " + minute);
    
    //Note that the minute is always incremented at least once
    //This avoids error when the new Snapshot is created at a 'whole' minute (a 
    //common occurrence).
    do {
      ++minute;
    }
    while (minute % fExposureTime != 0);
    fLogger.finest("Final minute : " + minute);
    
    result.set(Calendar.MINUTE, minute);
    return result.getTimeInMillis();
  }
  
  private Object[] getSignificantFields(){
    return new Object[]{
      fNumRequests, fEndTime, fAvgResponseTime, fMaxResponseTime, fExposureTime, fMaxUrl 
   };
  }
}
