package hirondelle.web4j.webmaster;

import hirondelle.web4j.util.Stopwatch;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;

import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 Compile simple performance statistics, and use periodic pings to detect trouble.

 <P>See <tt>web.xml</tt> for more information on how to configure this {@link Filter}.
 
 <h3>Performance Statistics</h3> 
 This class stores a <tt>Collection</tt> of {@link PerformanceSnapshot} objects in 
 memory (not in a database).

 <P>The presentation of these performance statistics in a JSP is always "one behind" this class. 
 This {@link Filter} examines the response time of each <em>fully processed</em> 
 request. Any JSP presenting the response times, however, is not fully processed <i>from the 
 point of view of this filter</i>, and has not yet contributed to the statistics.  

 <P><span class="highlight">It is important to note that {@link Filter} objects 
 must be designed to operate safely in a multi-threaded environment</span>.  
 Using the <a href='http://www.javapractices.com/Topic48.cjp'>nomenclature</a> of 
 <em>Effective Java</em>, this class is 'conditionally thread safe' : the responsibility 
 for correct operation in a multi-threaded environment is <em>shared</em> between 
 this class and its caller. See {@link #getPerformanceHistory} for more information.
 
<P> If desired, you can use also external tools such as 
<a href='http://www.siteuptime.com/'>SiteUptime.com</a> to monitor your site. 
*/
public final class PerformanceMonitor implements Filter {
  
  /**
   Read in the configuration of this filter from <tt>web.xml</tt>.
   
   <P>The config is validated, gathering of statistics is begun, and 
   any periodic ping operations are initialized.
  */
  public void init(FilterConfig aFilterConfig) {
    /*
     The logging performed here is not showing up in the expected manner. 
    */
    Enumeration items = aFilterConfig.getInitParameterNames();
    while ( items.hasMoreElements() ) {
      String name = (String)items.nextElement();
      String value = aFilterConfig.getInitParameter(name);
      fLogger.fine("Filter param " + name + " = " + Util.quote(value));
    }
    
    fEXPOSURE_TIME = new Integer( aFilterConfig.getInitParameter(EXPOSURE_TIME) );
    fNUM_PERFORMANCE_SNAPSHOTS = new Integer( 
      aFilterConfig.getInitParameter(NUM_PERFORMANCE_SNAPSHOTS) 
    );
    validateConfigParamValues();
    
    fPerformanceHistory.addFirst(new PerformanceSnapshot(fEXPOSURE_TIME));
  }
  
  /** This implementation does nothing.  */
  public void destroy() {
    //do nothing
  }
  
  /** Calculate server response time, and store relevant statistics in memory.   */
  public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain) throws IOException, ServletException {
    fLogger.fine("START PerformanceMonitor Filter.");
    
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    
    aChain.doFilter(aRequest, aResponse);
    
    stopwatch.stop();
    long millis = stopwatch.toValue()/(1000*1000);
    addResponseTime(millis, aRequest);
    fLogger.fine("END PerformanceMonitor Filter. Response Time: " + stopwatch);
  }

  /**
   Return statistics on recent application performance.
  
   <P>A static method is the only way an {@link hirondelle.web4j.action.Action} 
   can access this data, since it has no access to the {@link Filter} object 
   itself (which is built by the container).
   
   <P>The typical task for the caller is iteration over the return value. The caller  
   <b>must</b> synchronize this iteration, by obtaining the lock on the return value. 
   The typical use case of this method is :
   <PRE>
   List history = PerformanceMonitor.getPerformanceHistory();
   synchronized(history) {
     for(PerformanceSnapshot snapshot : history){
       //..elided
     }
   }
   </PRE>
  */
  public static List<PerformanceSnapshot> getPerformanceHistory(){
    /*
     Note that using Collections.synchronizedList here is not possible : the 
     API for that method states that when used, the returned reference must be used 
     for ALL interactions with the backing list. 
    */
    return fPerformanceHistory;
  }
  
  // PRIVATE 
  
  /**
   Holds queue of {@link PerformanceSnapshot} objects, including the "current" one.
  
  <P>The queue grows until it reaches a configured maximum length, after which stale 
   items are removed when new ones are added.
  
   <P>This mutable item must always have synchronized access, to ensure thread-safety.
  */
  private static final LinkedList<PerformanceSnapshot> fPerformanceHistory = new LinkedList<PerformanceSnapshot>();
  
  private static Integer fEXPOSURE_TIME;
  private static Integer fNUM_PERFORMANCE_SNAPSHOTS;
  
  /*
   Names of configuration parameters.
  */
  private static final String NUM_PERFORMANCE_SNAPSHOTS = "NumPerformanceSnapshots";
  private static final String EXPOSURE_TIME = "ExposureTime";
  
  private static final Logger fLogger = Util.getLogger(PerformanceMonitor.class);
  
  /**
   Validate the configured parameter values.
  */
  private void validateConfigParamValues(){
    StringBuilder message = new StringBuilder();
    if ( ! Util.isInRange(fNUM_PERFORMANCE_SNAPSHOTS, 1, 1000) ) {
      message.append(
        "web.xml: " + NUM_PERFORMANCE_SNAPSHOTS + " has value of " + 
        fNUM_PERFORMANCE_SNAPSHOTS + ", which is outside the accepted range of 1..1000."
      );
    }
    int exposure = fEXPOSURE_TIME;
    if ( exposure != 10 && exposure != 20 && exposure != 30 && exposure != 60){
      message.append(
        " web.xml: " + EXPOSURE_TIME + " has a value of " + exposure + "." + 
        " The only accepted values are 10, 20, 30, and 60."
      );
    }
    if ( Util.textHasContent(message.toString()) ){
      throw new IllegalArgumentException(message.toString());
    }
  }
  
  private static void addResponseTime(long aResponseTime /*millis*/, ServletRequest aRequest){
    long now = System.currentTimeMillis();
    HttpServletRequest request = (HttpServletRequest)aRequest;
    String url = WebUtil.getURLWithQueryString(request);
    //this single synchronization block implements the *internal* thread-safety 
    //responsibilities of this class
    synchronized( fPerformanceHistory ){
      if ( now > getCurrentSnapshot().getEndTime().getTime() ){
        //start a new 'current' snapshot
        addToPerformanceHistory( new PerformanceSnapshot(fEXPOSURE_TIME) );
      }
      updateCurrentSnapshotStats(aResponseTime, url);
    }
  }
  
  private static void addToPerformanceHistory(PerformanceSnapshot aNewSnapshot){
    while ( hasGap(aNewSnapshot, getCurrentSnapshot() ) ) {
      fLogger.fine("Gap detected. Adding empty snapshot.");
      PerformanceSnapshot filler = PerformanceSnapshot.forGapInActivity(getCurrentSnapshot());
      addNewSnapshot(filler);
    }
    addNewSnapshot(aNewSnapshot);
  }

  private static boolean hasGap(PerformanceSnapshot aNewSnapshot, PerformanceSnapshot aCurrentSnapshot){
    return aNewSnapshot.getEndTime().getTime() - aCurrentSnapshot.getEndTime().getTime() > fEXPOSURE_TIME*60*1000;
  }
  
  private static void addNewSnapshot(PerformanceSnapshot aNewSnapshot){
    fPerformanceHistory.addFirst(aNewSnapshot);
    ensureSizeRemainsLimited();
  }
  
  private static void ensureSizeRemainsLimited() {
    if ( fPerformanceHistory.size() > fNUM_PERFORMANCE_SNAPSHOTS ){
      fPerformanceHistory.removeLast();
    }
  }
  
  private static PerformanceSnapshot getCurrentSnapshot(){
    return fPerformanceHistory.getFirst();
  }
  
  private static void updateCurrentSnapshotStats(long aResponseTime /*millis*/, String aURL){
    PerformanceSnapshot updatedSnapshot = getCurrentSnapshot().addResponseTime(
      aResponseTime, aURL
    );
    //this style is needed only because the PerfomanceSnapshot objects are immutable.
    //Immutability is advantageous, since it guarantees that the caller 
    //cannot change the internal state of this class.
    fPerformanceHistory.removeFirst();
    fPerformanceHistory.addFirst(updatedSnapshot);
  }
}