/**
 * @fileoverview 
 * <p>This library is designed to encapsulate an xml http request call.
 * <ul>  
 *  <li>It centralizes the browser-specific logic of making this call.</li>
 *  <li>This library keeps the request bundled with it's callback methods, 
 *   making it easier to invoke and manage multiple xhr calls ( rather than using a global request ).</li>
 *  <li>It also provides useful utility functions, minimizing repeated business logic.</li>
 *  <li>The request classes in this library allow multiple StateChangeListeners
 *   to be registered, listening to the status of the request.  These state change handlers
 *   must implement one method, stateChange( StateChangeSupport container ).  This call sends a 
 *   reference to the support container for introspection.  The user of the XHR 
 *   class can optionally name it, for easy identification during callback, especially
 *   if the user is listening for multiple notifications.</li>
 *  <li>The StateChangeSupport class is an abstract class that expects to be extended.  XHR is
 *   the first such extension.  It provides a standard method for simple notification, as most 
 *   listeners are only interested in being notified of the start/stop of a request, rather than the inner specifics.
 *  <li>The XHR container can be, and is expected to be, reused.  Consecutive calls to the run( ) 
 *   method will re-initialize the container.  Small changes are expected between invocations of 
 *   run, especially action modifications ( new url params ).</li>
 *  </p>
 *  <p>Here's a simple invocation cycle, triggered by an input button, that can be run many times:<br /><br />
 * 
 *   var uptime = new XHRSuccessNotifyer( "check the server uptime" );<br />
 *   uptime.setAction( "http://localhost/getServerUptime" );<br />
 *   uptime.addListener( new XHRStateChangeListener( function( xhr ) { alert( "uptime:" + xhr.responseText ); } ) );<br />
 *    ....<br />
 *   <input type="button" name="uptime" value=" get uptime " onclick="uptime.run( null );" /><br />
 * 
 *  </p>
 * @author Robert D. Rice
 */

// vc_id = "$Id: xhr.js 12623 2009-09-21 21:46:29Z robert $"

// StateChangeSupport
var STATE_CHANGE_STATE_UNINITIALIZED = 0;
var STATE_CHANGE_STATE_LOADING = 1;
var STATE_CHANGE_STATE_LOADED = 2;
var STATE_CHANGE_STATE_INTERACTIVE = 3;
var STATE_CHANGE_STATE_COMPLETE = 4;
var STATE_CHANGE_STATUS_OK = 200;
var STATE_CHANGE_STATUS_ERROR = 400;
var STATE_CHANGE_STATUS_FORBIDDEN = 403;

var STATE_CHANGE_STATE_TEXT = new Array( );
STATE_CHANGE_STATE_TEXT[STATE_CHANGE_STATE_UNINITIALIZED] = "uninitialized";
STATE_CHANGE_STATE_TEXT[STATE_CHANGE_STATE_LOADING] = "loading   ";
STATE_CHANGE_STATE_TEXT[STATE_CHANGE_STATE_LOADED] = "loaded   ";
STATE_CHANGE_STATE_TEXT[STATE_CHANGE_STATE_INTERACTIVE] = "interactive";
STATE_CHANGE_STATE_TEXT[STATE_CHANGE_STATE_COMPLETE] = "complete";

var STATE_CHANGE_STATUS_TEXT = new Array( );
STATE_CHANGE_STATUS_TEXT[STATE_CHANGE_STATUS_OK] = "ok";
STATE_CHANGE_STATUS_TEXT[STATE_CHANGE_STATUS_ERROR] = "error";
STATE_CHANGE_STATUS_TEXT[STATE_CHANGE_STATUS_FORBIDDEN] = "forbidden";

// XHR
var XHR_METHOD_GET = "GET";
var XHR_METHOD_POST = "POST";
var XHR_METHOD_HEAD = "HEAD";
var XHR_METHOD_PUT = "PUT";
var XHR_METHOD_DELETE = "DELETE";

/**
 * Constructor for StateChangeSupport abstract class.
 * Children of this class should:
 *  - initialize the listeners array ( required )
 *  - initialize the name attribute ( optional )
 *  - add impl. for state ( 0, 1, 2, 3, 4 )
 *  - add impl. for status ( 200, 400 )
 * @constructor
 * @param name for this container, optional
 * @return new StateChangeSupport instance
 */
function StateChangeSupport( name  ) {
    // attributes
    // StateChangeListener objects.
    // require one method, stateChange( StateChangeSupport container )
    this.listeners = new Array( );
    this.name = name ? name : null;
    this.state = STATE_CHANGE_STATE_UNINITIALIZED;
    this.status = STATE_CHANGE_STATUS_OK;

    // methods
    this.addListener = StateChangeAddListener;
    this.removeListener = StateChangeRemoveListener;
    this.fireChange = StateChangeFireChange;
    this.success = StateChangeSuccess;
    this.complete = StateChangeComplete;
    this.getState = StateChangeGetState;
    this.setState = StateChangeSetState;
    this.stateText = StateChangeStateText;
    this.getStatus = StateChangeGetStatus;
    this.setStatus = StateChangeSetStatus;
    this.statusText = StateChangeStatusText;
}

/**
 * Add a StateChangeListener
 * @param the StateChangeListener object
 */
function StateChangeAddListener( listener ) {
    this.listeners[ this.listeners.length ] = listener;
    
    // XHRStateChange is deprecated.  until it is removed
    if ( listener.XHRStateChange ) { listener.stateChange = listener.XHRStateChange; }
}

/**
 * Remove a StateChangeListener
 * @param the StateChangeListener object
 */
function StateChangeRemoveListener( listener ) {
    var temp = this.listeners;
    this.listeners = new Array( );
    for ( var i = 0; i < temp.length; i++ ) {
	if ( temp[i] != listener ) { 
	    this.addListener( temp[i] );
	}
    }
}

/**
 * Fire a StateChangeEvent to the listeners
 * This is simply a reference to the StateChangeSupport container
 */
function StateChangeFireChange( ) {
  for ( var i = 0; i < this.listeners.length; i++ ) {
    this.listeners[ i ].stateChange( this );
  }
}

/**
 * Get the state
 * @return state ( 0, 1, 2, 3, 4 )
 */
function StateChangeGetState( ) {
    return this.state;
}

/**
 * Set the state
 * @param state ( 0, 1, 2, 3, 4 )
 */
function StateChangeSetState( state ) {
    this.state = state;
    this.fireChange( );
}

/**
 * Get the status
 * @return status ( 200, 400 )
 */
function StateChangeGetStatus( ) {
    return this.status;
}

/**
 * Set the status
 * @param status ( 200, 400 )
 */
function StateChangeSetStatus( status ) {
    this.status = status;
    this.fireChange( );
}

/**
 * Is the request ready.
 * This is independent of level of success.
 * @param true if complete
 */
function StateChangeComplete( ) {
  return this.getState( ) == STATE_CHANGE_STATE_COMPLETE ? true : false;
}

/**
 * Is the request ready and ok
 * @param true if complete and without error
 */
function StateChangeSuccess( ) {
  return ( this.complete( ) && this.getStatus( ) < STATE_CHANGE_STATUS_ERROR ) ? true : false;
}

/**
 * Get the state text
 * @return text translation of the state code
 */
function StateChangeStateText( ) {
  return STATE_CHANGE_STATE_TEXT[this.getState( )]; 
}

/**
 * Get the status text
 * @return text translation of the status code
 */
function StateChangeStatusText( ) {
  return STATE_CHANGE_STATUS_TEXT[this.getStatus( )]; 
}

/**
 * Constructor for a simple StateChangeListener
 * This is provided as a convenience.  It wraps the
 * passed function, to be called properly by StateChangeSupport.
 * This one method accepts an StateChangeSupport object as the only param.
 * @constructor
 * @param function pointer, can be anonymous
 * @return new StateChangeListener
 */
function StateChangeListener( stateChange ) {
  this.stateChange = stateChange;
}

/**
 * Constructor for the XMLHttpRequest container
 * @constructor
 * @param name for this container, optional
 * @return new XHR instance.
 */
function XHR( name ) {
  // attributes
  // StateChangeListener objects.
  this.listeners = new Array( );
  this.name = name ? name : null;

  this.action = null;
  this.method = XHR_METHOD_GET;
  this.asynch = true;
  this.exception = null;
  this.track = true;

  // inner XMLHttpRequest object
  this.request = null;

  // methods
  this.getState = XHRGetState;
  this.getStatus = XHRGetStatus;

  this.setAction = XHRSetAction;
  this.getAction = XHRGetAction;
  this.setMethod = XHRSetMethod;
  this.setTrack = XHRSetTrack;
  this.addListener( new StateChangeListener( XHRFireTracker ) );
  this.createRequest = XHRCreateRequest;
  this.init = XHRInit;
  this.send = XHRSend;
  this.run = XHRRun;
  this.responseText = XHRResponseText;
  this.responseXML = XHRResponseXML;
}
XHR.prototype = new StateChangeSupport( );

/**
 * set the action of the request
 * @param string action
 */
function XHRSetAction( action ) {
  this.action = action;
}

/**
 * get the action of the request
 * @return string action
 */
function XHRGetAction(  ) {
  return this.action;
}

/**
 * set the method of the request
 * @param string method
 */
function XHRSetMethod( method ) {
  this.method = method;
}

/**
 * set the state of the track flag.
 * will autotrack by default.
 * default tracking expects that hitReport.js exists
 * @param boolean true to track
 */
function XHRSetTrack( track ) {
    this.track = track;
}

/**
  * utility method to create an XMLHttpRequest.
  * @return request or null
  * @type XMLHttpRequest
  */
function XHRCreateRequest( ) {
  return window.ActiveXObject ? 
    new ActiveXObject( "Microsoft.XMLHTTP" ) :
    ( window.XMLHttpRequest ? new XMLHttpRequest( ) : null );
}

/**
 * utility method to check for xhr browser compatibility, 
 * and redirect to a new page if there is no support.
 * @param url to redirect to
 */
function XHRCheck( url ) {
  if ( XHRCreateRequest( ) == null ) {
    location = url;
  }
}

/**
  * method to initialze the dialog with the server
  */
function XHRInit(  ) {
  this.request = this.createRequest( );
  // we may want to wrap the following in a reset method, 
  // to abort any running requests.
  this.exception = null;
  if ( this.request != null ) {
    try {
      this.request.open( this.method, this.action, this.asynch );
      var xhr = this;
      this.request.onreadystatechange = function( ) {
        xhr.fireChange.call( xhr );
      }
      this.request.setRequestHeader( "Content-Type",
                                     "application/x-www-urlencoded" );
    } catch ( ex ) {
      this.exception = ex;
      this.fireChange( );
    } 
  }
}

/**
  * send content to the server
  * @param String content
  */
function XHRSend( content ) {
  if ( this.request != null &&
       this.exception == null ) {
    try {
      this.request.send( content );
    } catch ( ex ) {
      this.exception = ex;
      this.fireChange( );
     }
  }
}

/**
  * run method.  this is a convenience
  * method that initializes the stream 
  * and sends the content
  * @param String content
  */
function XHRRun( content ) {
  this.init( );
  this.send( content );
}

/**
 * Get the state
 * @return state ( 0, 1, 2, 3, 4 )
 */
function XHRGetState( ) {
    var rcode = STATE_CHANGE_STATE_UNINITIALIZED;
    if ( this.request != null ) { rcode = this.request.readyState; }
    return rcode;
}

/**
 * Get the status
 * @return status ( 200, 400 )
 */
function XHRGetStatus( ) {
    var rcode = STATE_CHANGE_STATUS_ERROR;
    if ( this.request != null ) { rcode = this.request.status; }
    return rcode;
}

/**
 * Get the response text
 * @return text, if avail.
 */
function XHRResponseText( ) {
  var rcode = null;
  if ( this.request != null &&
       this.request.readyState == STATE_CHANGE_STATE_COMPLETE ) {
    rcode = this.request.responseText;
  } 
  return rcode;
}

/**
 * Get the response xml
 * @return xml, if avail.
 */
function XHRResponseXML( ) {
  var rcode = null;
  if ( this.request != null &&
       this.request.readyState == STATE_CHANGE_STATE_COMPLETE ) {
    rcode = this.request.responseXML;
  } 
  return rcode;
}

/**
 * Fire the Hit Tracker on behalf of this request
 * @param scs
 */
function XHRFireTracker( scs ) {
    // do we track complete or only success state?
    if ( scs.complete( ) && scs.track ) {
	var path = scs.getAction( );
	var type = "track=ajax";
	if ( path.indexOf( "?" ) == -1 ) {
	    path = path + "?" + type;
	} else if ( path.indexOf( "?" ) == ( path.length - 1 ) ) {
	    path = path + type;
	} else {
	    path = path + "&" + type;
	}
        captureHitStats( path );
    }
}

/**
 * Constructor for the XMLHttpRequest container,
 * that only notifies upon successful completion.
 * @constructor
 * @param name for this container, optional
 * @return new XHR instance.
 */
function XHRSuccessNotifyer( name ) {   
  this.name = name ? name : null;
}
XHRSuccessNotifyer.prototype = new XHR( );
XHRSuccessNotifyer.prototype.fireChange = function( ) {
  if ( this.success( ) ) {
    var size = this.listeners.length;
    for ( var i = 0; i < size; i++ ) {
      this.listeners[ i ].stateChange( this );
    }
  }
}

/**
 * Constructor for a simple XHRStateChangeListener
 * This is provided as a convenience.  It wraps the
 * passed function, to be called properly by XHR.
 * This one method accepts an XHR object as the only param.
 * @constructor
 * @param function pointer, can be anonymous
 * @return new XHRStateChangeListener
 * @deprecated use generic StateChangeListener instead
 */
function XHRStateChangeListener( stateChange ) {
  this.stateChange = stateChange;
}
XHRStateChangeListener.prototype = new StateChangeListener( );

/**
 * get the first element's value by name, from xml
 * @param xml dom
 * @param name of element
 * @param default to return if value not found, optional, returns null otherwise
 * @param if true, interpret as numeric, this will convert the string to a number
 * @param if true, preserve empty nodes, return empty string "" or numeric 0 rather than default
 * @return value or default
 */
function XHRFirstElementsValueByName( xml, name, def, numeric, empty ) {
    var rval = def;
    var els = xml.getElementsByTagName( name );
    if ( els.length > 0 ) {
        if ( els[0].childNodes.length > 0 ) {
	    rval = numeric ? XHRNodeValueNumber( els[0].childNodes[0] ) : els[0].childNodes[0].nodeValue;
	} else if ( empty ) {
	    rval = numeric ? 0 : "";
	}
    }
    return rval;
}

/**
 * Get the node value as a number
 * @param xml node
 * @return number, rather than text
 */
function XHRNodeValueNumber( node ) {
  var i = node.nodeValue;
  i = i - 0;
  return i;
}

/**
 * Frame Loader, with state change support.
 * This class is generic, can be used to load any frame with callback.
 * @param name of the loader
 * @param id of the frame object
 */
function FrameLoaderSCS( name, id ) {
    // attributes
    // StateChangeListener objects.
    // require one method, stateChange( StateChangeSupport container )
    this.listeners = new Array( );
    this.name = name ? name : null;
    this.state = STATE_CHANGE_STATE_UNINITIALIZED;
    this.status = STATE_CHANGE_STATUS_OK;
    this.id = id;

    this.load = FrameLoaderSCSLoad;
    this.loaded = FrameLoaderSCSLoaded;
}
FrameLoaderSCS.prototype = new StateChangeSupport( );

/**
 * load the frame
 * @param url to load
 */
function FrameLoaderSCSLoad( url ) {
    this.setState( STATE_CHANGE_STATE_LOADING );
    var scs = this;
    document.getElementById( this.id ).onload = function( ) { scs.loaded.call( scs ); };
    document.getElementById( this.id ).onreadystatechange = 
      	function ( ) { if ( this.readyState == 'complete' || this.readyState == 'loaded' ) { 
 	scs.loaded.call( scs ); } };
    document.getElementById( this.id ).src = url;
}

/**
 * frame is loaded callback
 */
function FrameLoaderSCSLoaded( ) {
    this.setState( STATE_CHANGE_STATE_COMPLETE );
}

/**
 * Pagination with state change support
 * @param name of this pagination
 */
function PaginationSCS( name ) {
    // attributes
    // StateChangeListener objects.
    // require one method, stateChange( StateChangeSupport container )
    this.listeners = new Array( );
    this.name = name ? name : null;
    this.state = STATE_CHANGE_STATE_UNINITIALIZED;
    this.status = STATE_CHANGE_STATUS_OK;

    /** pagination attributes */
    this.size = 0;
    this.index = 0;
    this.wrap = 0;
    
    /** pagination methods */
    this.nextPage = PaginationSCSNextPage;
    this.prevPage = PaginationSCSPrevPage;
    this.setPage = PaginationSCSSetPage;
    this.getPage = PaginationSCSGetPage;
    this.setSize = PaginationSCSSetSize;
    this.getSize = PaginationSCSGetSize;
    this.isWrap = PaginationSCSIsWrap;
    this.setWrap = PaginationSCSSetWrap;
}
PaginationSCS.prototype = new StateChangeSupport( );

/**
 * flip to the next page
 */
function PaginationSCSNextPage( ) {
    this.setPage( this.getPage( ) + 1 );
}

/**
 * flip to the prev page
 */
function PaginationSCSPrevPage( ) {
    this.setPage( this.getPage( ) - 1 );
}

/**
 * set the index
 * this is the workhorse method that performs
 * error handling, incorporates the business rules
 * @param requested index
 * @param force, if true, ignore the business rules 
 *  and accept the value as is, will always succeed
 * @param silent, if true, supress notification
 */
function PaginationSCSSetPage( idx, force, silent ) {
    if ( !silent ) { this.setState( STATE_CHANGE_STATE_LOADING ); }
    var completeStatus = STATE_CHANGE_STATUS_OK;

    /** assure a number */
    idx = idx - 0;

    if ( force ) {
	this.index = idx;
    } else {
	/** process wrapping */
	if ( this.getSize( ) && this.isWrap( ) && idx > this.getSize( ) ) {
	    idx = 1;
	} else if ( this.getSize( ) && this.isWrap( ) && idx < 1 ) {
	    idx = this.getSize( );
	}
    
	/** core processing */
	if ( idx >= 1 && idx <= this.getSize( ) ) {
	    this.index = idx;
	} else {
	    completeStatus = STATE_CHANGE_STATUS_ERROR;
	}
    }

    if ( !silent ) {
	this.setStatus( completeStatus );
	this.setState( STATE_CHANGE_STATE_COMPLETE );
    }
}

/**
 * get the index
 * @return the page index
 */
function PaginationSCSGetPage( ) {
    return this.index;
}

/**
 * set the size
 * this is the workhorse method that performs
 * error handling, incorporates the business rules
 * @param size
 * @param force, if true, ignore the business rules 
 *  and accept the value as is, will always succeed
 * @param silent, if true, supress notification
 */
function PaginationSCSSetSize( size, force, silent ) {
    if ( !silent ) { this.setState( STATE_CHANGE_STATE_LOADING ); }
    var completeStatus = STATE_CHANGE_STATUS_OK;

    /** assure a number */
    size = size - 0;

    if ( size >= 0 || force ) {
	this.size = size; 
    } else {
	completeStatus = STATE_CHANGE_STATUS_ERROR;
    }

    if ( !silent ) {
	this.setStatus( completeStatus );
	this.setState( STATE_CHANGE_STATE_COMPLETE );
    }
}

/**
 * get the size
 * @return size
 */
function PaginationSCSGetSize( ) {
    return this.size;
}

/**
 * are we allowed to wrap
 * @return true if wrap enabled
 */
function PaginationSCSIsWrap( ) {
    return this.wrap;
}

/**
 * set the wrap flag
 * @param wrap flag
 */
function PaginationSCSSetWrap( wrap ) {
    this.wrap = wrap - 0;
}


