/**
    Ajax client framework for RPC calls to the server.
    Copyright (C) 2009 Siteseeing Internet Services

    A compact, efficient ajax communication framework.
    - send RPC calls to the server.
    - process RPC response calls from the server.
    - runtime checks to ease development.
    - 4Kb minified.
*/

if( ! window.sis ) { window.sis = {}; }


// Version history:
// 2009-06-19 v1.1: update onReplaceHtml() callback.
//                  - added initNow argument
//                  - pass elementId in callback.
//                  - set 'this' to the clientObject
//                  added executeCalls()
// 2009-10-15 v1.2: return XHR object in callMethod()


/**
 * Constructor
 */
sis.ajax = function( clientObject, ajaxUrl )
{
  // Verify server URL. allow 
  // Check whether there would be security errors during the server call.
  if( ajaxUrl && ajaxUrl.charAt(0) != "/" )
  {
    var url = location.href.substring( 0, location.href.indexOf("/", 9 ) );
    if( ajaxUrl.substring( 0, url.length ) != url )
    {
      if( window.console ) console.error("sis.ajax error: the remote server URL does not use the same domain name!");
    }
  }

  this._ajaxUrl      = ajaxUrl;
  this._clientObject = clientObject;
  this._userMessage = "Helaas...\n\n"
                    + "Er is een fout opgetreden bij het communiceren met de server.\n"
                    + "Probeer het later a.u.b. nogmaals";

  // Create closures only once
  var myself = this;
  this._handleAjaxResponse    = function( data )   { return myself._ajaxResponse( data ); };
  this._handleAjaxError       = function( x,s,e )  { return myself._ajaxError( x,s,e); };
  this._handleShowSpinner     = function()         { return myself._showSpinner(); }
}



/**
 * Serialize a form to an array of fields.
 * This is a variant of jQuery.fn.serializeArray() which is compatible with Konqueror too.
 */
sis.ajax.getFormData = function(form)
{
  // Allow jquery arg.
  if( typeof(form) == "object" && form.jquery )
    form = form[0];

  var values = [];

  for( var i = 0; i < form.elements.length; i++ )
  {
    var el = form.elements[i]
    if( ! el.type || el.disabled ) continue;  // fieldset, or disabled

    switch( el.type )
    {
      case "select-multiple":
        for( var j = 0; j < el.options.length; j++ )
        {
          var op = el.options[ j ];
          if( op.selected )
            values.push( { name: el.name, value: op.value } );
         }
        break;
      case "submit": case "button": case "reset": break;
      case "radio": case "checkbox": if( ! el.checked ) break;
      default:  // hidden, text, password, search, textarea, select-one
        values.push( { name: el.name, value: el.value } );
        break;
    }
  }

  return values;
}



jQuery.extend( sis.ajax.prototype, 
{
  version: 1.2   // 15 okt 2009
, _debug: ( window.console != null )
, _clientObject: null
, _ajaxUrl:  ""
, _inResponse: false
, _spinnerTimer: 0
, _events: { showSpinner: [], hideSpinner: [], replaceHtml: [] }


  /**
   * Make a call to the server.
   * The params can be a "postdata" string, JavaScript object literal, or array of key/value pairs.
   * Returns the XmlHttpRequest object if the call started.
   */
, callMethod: function( methodName, params )
  {
    if( this._ajaxUrl == "" )
    {
      if( this._debug ) console.error("sis.ajax.callMethod() - no ajaxUrl defined!");
      alert( this._userMessage );
      return null;
    }

    // Build postdata
    // pass language from <html lang=".."> to server messages are localized too.
    var postdata = "$call=" + methodName + "&$lang=" + document.documentElement.lang;
    if( params != null )
     {
      var paramType = typeof( params );
      postdata += "&" + ( typeof( params ) == "string" ? params : jQuery.param( params ) );
      if( postdata.indexOf("&undefined=") != -1 )
      {
        if( this._debug ) console.error("sis.ajax.callMethod() - invalid parameters provided: ", params);
        alert( this._userMessage );
        return null;
      }
    }

    clearTimeout( this._spinnerTimer );
    this._spinnerTimer = setTimeout( this._handleShowSpinner, 150 );
    return jQuery.ajax( { type:     "POST"
                        , url:      this._ajaxUrl
                        , data:     postdata
                        , success:  this._handleAjaxResponse
                        , error:    this._handleAjaxError
                        , dataType: "json"
                        } );
  }


  /**
   * Make a custom ajax request.
   * This is a proxy for jQuery.ajax, which adds
   * the spinner, default error handler and "in response" state.
   */
, custom: function( params )
  {
    clearTimeout( this._spinnerTimer );
    this._spinnerTimer = setTimeout( this._handleShowSpinner, 150 );

    // Proxy the success callback to hide the spinner.
    var myself = this;
    if( params.success )
    {
      var successCallback = params.success;
      params.success = function( d, s ) { myself._customAjaxResponse( d, s, successCallback ); };
    }
    
    // Auto fill in with defaults
    if( ! params.error )
    {
      params.error = this._handleAjaxError;
    }
    else
    {
      var errorCallback = params.error;
      params.error = function( x, s, e ) { myself._hideSpinner(); errorCallback( x, s, e ); };
    }

    // Make request
    return jQuery.ajax( params );
  }


  /**
   * Register a callback when a "spinner image" or "loading..." sign should be shown.
   * In constrast to jQuery.fn.ajaxStart, this function is called when the requests takes more time.
   */
, onShowSpinner: function( callback )
  {
    this._events.showSpinner.push( callback );
  }


  /**
   * Register a callback when the spinner image should be hidden.
   */
, onHideSpinner: function( callback )
  {
    this._events.hideSpinner.push( callback );
  }


  /**
   * Register a callback when a HTML tag is replaced.
   */
, onReplaceHtml: function( elementId, callback, initNow )
  {
    if( typeof( callback ) != "function" )
    {
      if( this._debug )
        console.error("sis.ajax.onReplaceHtml() - expected a function as argument.");

      return;
    }

    this._events.replaceHtml.push( [ elementId, callback ] );

    if( initNow )
    {
      callback.call( this._clientObject, elementId );
    }
  }


  /**
   * Process a response, possibly outside the ajax response
   */
, executeCalls: function SisAjax_executeCalls( calls )
  {
    // Process call calls.
    for( var i in calls )
    {
      // Convert the arguments back to an array.
      var call = calls[ i ]
      var args = [];
      for( var j in call.args )
      {
        args.push( call.args[ j ].v );
      }

      // Special cases for internal methods.
      // Normal situal
      var targetObject = ( call.type == "s" ? this : this._clientObject );

      // Call the method of the client object.
      if( targetObject[ call.method ] != null
      &&  typeof( targetObject[ call.method ] ) == "function" )
      {
//        if( this._debug ) console.log("_gotResponse() - Calling: ", call.method, args);
        targetObject[ call.method ].apply( targetObject, args );
      }
      else if( this._debug )
      {
        console.error("Unable to handle response, no such method: clientObject." + call.method + "()" );
      }
    }
  }



  // -----------------------------------
  // Internal API for ajax processing

  /**
   * Return true if this object is currently processing the response.
   * Use this to find out if your method is called by the server, or from a local call.
   */
, isProcessingResponse: function()
  {
    return this._inResponse;
  }


  /**
   * The server returns a JSON object with the local methods to call.
   */
, _ajaxResponse: function SisAjax__ajaxResponse( data )
  {
    this._inResponse = true;

    // Avoid showing the spinner
    clearTimeout( this._spinnerTimer );
    this._spinnerTimer = 0;

    // Execute calls
    this.executeCalls( data.calls );

    // Hide spinner
    this._hideSpinner();
    this._inResponse = false;
  }


  /**
   * A custom ajax response.
   */
, _customAjaxResponse: function SisAjax__ajaxCustomResponse( data, statusText, callback )
  {
    this._inResponse = true;

    // Avoid showing the spinner
    clearTimeout( this._spinnerTimer );
    this._spinnerTimer = 0;

    // Execute calls
    callback( data, statusText );

    // Hide spinner
    this._hideSpinner();
    this._inResponse = false;
  }


  /**
   * The server had an internal error.
   */
, _ajaxError: function SisAjax__ajaxError(xhr, status, error)
  {
    // Show response message in the console.
    if( xhr.status == 500 )
    {
      var response = xhr.responseText;
      response = response.replace( /\r?\n/g, "" )
                         .replace( /(<\/)/g, "\n$1" )
                         .replace( /<[^>]+>/g, "" )
                         .replace( /Error in HareScript file/, "" )
                         .trim();
      if( this._debug )
      {
        console.error("Error while processing server request. " + response.substring(0, response.indexOf("\n")));
        console.info( response );   // to deal with Firebug 1.4, trimming error messages.
      }
    }

    this._hideSpinner();
    alert( this._userMessage );
  }

, _showSpinner: function()
  {
    var callbacks = this._events.showSpinner
    for( i in callbacks )
      callbacks[i].call( this._clientObject );
  }
 
, _hideSpinner: function()
  {
    var callbacks = this._events.hideSpinner;
    for( i in callbacks )
      callbacks[i].call( this._clientObject );
  }


  // -----------------------------------
  // Default calls from the server

, alert: function( message )
  {
    this._hideSpinner();
    window.alert( message );
  }

, debug: function( type, message )
  {
    if( ! this._debug ) return;

    switch( type )
    {
      case "print":
        console.log( "** SERVER PRINT OUTPUT **\n\n" + message );
        break;

      default:
        console.log( message );
    }
  }

, replaceHtml: function( elementId, newHtml )
  {
    // Get the element.
    var el = document.getElementById( elementId );
    if( el == null )
    {
      if( this._debug )
        console.error("sis.ajax.replaceHtml() - the element '" + elementId + "' does not exist.");

      return;
    }

    // Replace HTML
    $(el).replaceWith( newHtml );

    // Make callbacks for all replaced items.
    var callbacks = this._events.replaceHtml;
    for( i in callbacks )
    {
      var slot = callbacks[i];
      if( slot[0] == elementId )
        slot[1].call( this._clientObject, elementId );
    }
  }

} );


// WebKit debugging enhancement:
(function(){
  var p = sis.ajax.prototype;
  for( i in p )
    if( typeof( p[i] ) == 'function' )
      p[i].displayName = "sis.ajax." + i;
})();

