/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

/**
 * @private
 */
var BHF = {};

/**
 * @private
 */
BHF.Object = function(o)
{
    var F = function() {};
    F.prototype = o;
    return new F();
};

/**
 * This little ditty 'captures' the YAHOO libs currently loaded and allows the app to use other versions
 * @private
 */
(function()
{
    var ns;
    for( ns in YAHOO ) if( YAHOO.lang.hasOwnProperty( YAHOO, ns ) )
        BHF[ns] = YAHOO[ns];
    BHF.lang.isDefined = function(o){return o != null && !BHF.lang.isUndefined(o);};
    /**
     * Insert a script into the given window.
     * @param url URL to the script
     * @param win optional Window
     */
    BHF.lang.insertScript = function( url, win )
    {
        var w = win || window, d=w.document, n=d.createElement("script"),
            h = d.getElementsByTagName("head")[0];

        n.src = url;
        n.type = "text/javascript";
        h.appendChild(n);
    };
})();

/**
 * We modify the JSON parsing slightly to accept 'undefined' and 'null'.
 */
BHF.lang.JSON.parseJSON = function( s, filter )
{
    if( s == "undefined" )
        return undefined;

    if( s == "null" )
        return null;

    return YAHOO.lang.JSON.parse( s, filter );
}

/**
 * Construct a <code>Service</code> object.
 * @param url {string} Absolute or page relative URL to the Service. Note that the Service must be in the same
 *  domain
 * @public
 * @class Service
 */
BHF.Service = function( url )
{
    var lockPolicy = BHF.Service.DISPATCH_POLICY.SERIAL;

    var insertMethodIntoURL = function( url, name )
    {
        return url.replace( /{method}/, name );
    };

    /**
     * Queue for invocation requests waiting to be dispatched and those that have been dispatched but not yet
     * returned. These queues contain txn objects as follows
     *  txn.service     A reference to the service
     *  txn.name        The method name
     *  txn.parameters  Unresolved (raw) parameters
     *  txn.callback    The callback object (sucess, failure, precondition, and rollback)
     *  txn.resolved    Resolved parameters (valid only after the call has been dispatched)
     *  txn.conn        The Yahoo! connect result object (valid after the call has been dispatched)
     *  txn.abort()     Abort an outstanding call.
     * @private
     */
    var waiting = [];

    /**
     * @private
     */
    this.isInline = function( callback, args )
    {
        return args.length < 2 && (BHF.lang.isUndefined( callback ) || (
            !BHF.lang.isFunction( callback.success ) &&
            !BHF.lang.isFunction( callback.failure ) &&
            !BHF.lang.isFunction( callback.precondition ) &&
            !BHF.lang.isFunction( callback.rollback )));
    };

    /**
     * @private
     */
    this.removeFromQueue = function( txn )
    {
        for( var i=0,l=waiting.length; i < l; i++ )
            if( txn === waiting[i] )
            {
                waiting.splice( i, 1 );
                return;
            }
    };

    /**
     * @private
     */
    this.hasPendingRequest = function( xhr )
    {
        for( var i=0,l=waiting.length; i < l; i++ )
            if( BHF.lang.isDefined( waiting[i].conn ) )
                return true;
        return false;
    };

    /**
     * @private
     */
    this.nextToDispatch = function( xhr )
    {
        for( var i=0,l=waiting.length; i < l; i++ )
            if( !BHF.lang.isDefined( waiting[i].conn ) )
                return waiting[i];
        return undefined;
    };

    /**
     * @private
     */
    this.getXHRResult = function( xhr )
    {
        var t = xhr.responseText;
        var o = t;

        // Sometimes the content type includes character encoding ?!?!. So we only check a contains
        if( !BHF.lang.isUndefined( t ) && BHF.lang.isDefined( xhr.getResponseHeader[ 'Content-Type' ] ) && xhr.getResponseHeader[ 'Content-Type' ].indexOf( 'text/javascript' ) >= 0 )
        {
            o = BHF.lang.JSON.parseJSON( t );
        }

        return o;
    };

    /**
     * @private
     */
    this.agumentWithDefaults = function( callback )
    {
        if( BHF.lang.isDefined( this.defaults ) )
        {
            for( var k in this.defaults ) if( BHF.lang.hasOwnProperty( this.defaults, k ) )
            {
                if( BHF.lang.isUndefined( callback[k] ) )
                    callback[k] = this.defaults[k];
            }
        }
    }

    /**
     * Resolve a parameter object by invoking lazy eval functions and converting objects to JSON representation,
     * effectively capturing the state that will be sent to the service. The resolved parameters include an
     * encode method.
     * @private
     */
    this.resolve = function( params )
    {
        var k,v,p={},a=[];

        for( var k in params ) if( BHF.lang.hasOwnProperty( params, k ) )
        {
            var v = params[k];
            if( !BHF.lang.isUndefined( v ) )
            {
                if( BHF.lang.isFunction( v ) ) // Lazy fetch
                {
                    v = v();
                }

                if( BHF.lang.isObject( v ) ) // JSON objects
                {
                    v = YAHOO.lang.JSON.stringify( v );
                }

                p[k]=v;
                a.push( encodeURIComponent( k ) + '=' + encodeURIComponent( v ) );
            }
        }

        p.encode = function()
        {
            return a.join('&');
        };

        return p;
    };

    /**
     * @private
     */
    this.clearQueue = function()
    {
        for( var i=0,l=waiting.length; i < l; i++ )
            if( BHF.lang.isDefined( waiting[i].conn ) )
                BHF.util.Connect.abort( waiting[i].conn );
        waiting = [];
    };

    /**
     * @private
     */
    this.checkPreconditions = function( txn )
    {
        var ok = true;

        if( BHF.lang.isFunction( txn.callback.precondition ) )
        {
            ok = txn.callback.precondition( txn );
        }

        return ok;
    };

    /**
     * @private
     */
    this.nextInQueue = function()
    {
        var txn     = this.nextToDispatch();
        if( BHF.lang.isUndefined( txn ) )
            return;

        var adapter = BHF.Object( txn.callback ); // We'll wrap some of the callbacks
        var that    = this;

        adapter.success = function( xhr )
        {
            try
            {
                if( !that.checkPreconditions( txn ) )
                {
                    if( lockPolicy === BHF.Service.DISPATCH_POLICY.SERIAL )
                        that.clearQueue();
                    if( BHF.lang.isFunction( txn.callback.rollback ) )
                        txn.callback.rollback( txn );
                }
                else if( BHF.lang.isFunction( txn.callback.success ) )
                {
                    txn.callback.success( that.getXHRResult( xhr ), xhr );
                }
            }
            catch( e )
            {
                if( lockPolicy === BHF.Service.DISPATCH_POLICY.SERIAL )
                    that.clearQueue();
            }
            finally
            {
                that.removeFromQueue( txn );
                that.nextInQueue();
            }
        };

        adapter.failure = function( xhr )
        {
            try
            {
                if( lockPolicy === BHF.Service.DISPATCH_POLICY.SERIAL )
                    that.clearQueue();
                if( BHF.lang.isFunction( txn.callback.failure ) )
                    txn.callback.failure( that.getXHRResult( xhr ), xhr );
            }
            finally
            {
                that.removeFromQueue( txn );
                that.nextInQueue();
            }
        };

        adapter.customevents = {
	        onAbort:function()
            {
                try
                {
                    if( BHF.lang.isFunction( txn.callback.abort ) )
                        txn.callback.abort( that.getXHRResult( xhr ), xhr );
                }
                finally
                {
                    if( lockPolicy === BHF.Service.DISPATCH_POLICY.SERIAL )
                    {
                        txn.service.clearQueue();
                    }
                    else
                    {
                        BHF.util.Connect.abort( txn.conn );
                        txn.service.removeFromQueue( txn );
                    }
                }
            }
	    };

        txn.resolved = this.resolve( txn.parameters ); // Resolve at dispatch
        txn.conn = BHF.util.Connect.asyncRequest( 'POST', insertMethodIntoURL( url, txn.name ), adapter, txn.resolved.encode() );
    };

    /**
     * @private
     */
    this.invoke = function( name, callback, parameters )
    {
        var txn =
        {
            service:    this,
            name:       name,
            callback:   callback,
            parameters: parameters
        };

        waiting.push( txn );

        if( lockPolicy === BHF.Service.DISPATCH_POLICY.CONCURRENT || !this.hasPendingRequest() )
        {
            this.nextInQueue();
        }

        return txn;
    };

    /**
     * @private
     */
    this.invokeSync = function( name, parameters )
    {
        var result;
        var exception   = false;
        var connection;
        var postData    = parameters ? this.resolve( parameters ).encode() : null;
        var that        = this;

        var adapter =
        {
            success: function( xhr )
            {
                result = that.getXHRResult( xhr );
            },

            failure: function( xhr )
            {
                result = that.getXHRResult( xhr );
                exception = true;
            }
        }

        // Fragile. getConnectionObject() and handleTransactionResponse() are not really public
        connection = BHF.util.Connect.getConnectionObject( false );
        connection.conn.open( 'POST', insertMethodIntoURL( url, name ), false );
        connection.conn.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
        connection.conn.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
        connection.conn.send( postData );
        BHF.util.Connect.handleTransactionResponse( connection, adapter, false );

        if( exception )
            throw result;

        return result;
    }

    this._setDispatchPolicy = function( policy )
    {
        if( policy != BHF.Service.DISPATCH_POLICY.SERIAL && policy != BHF.Service.DISPATCH_POLICY.CONCURRENT )
            throw { name: "Illegal lock policy", message: "Lock policies must be BHF.Service.DISPATCH_POLICY.SERIAL or BHF.Service.DISPATCH_POLICY.CONCURRENT" }
        lockPolicy = policy;
    }
};

BHF.Service.prototype =
{
    /**
     * Set the default callback function for this <code>Service</code>. These may be overridden when a call is made.

     * @param callback Object containing one or more of: success, failure, precondition, or rollback.
     * @public
     */
    setDefaultCallback: function( callback )
    {
        this.defaults = {};
        for( var k in callback ) if( BHF.lang.hasOwnProperty( callback, k ) )
            this.defaults[k] = callback[k];
    },

    /**
     * <p>Add a method to the <code>Service</code> object. The method may subsequently be invoked by name synchronously
     * (myService.foo(x)) or asynchronously (myService.foo( callbacks, x )).</p>
     * <h3>Synchronous</h3>
     * <p>During a synchronous call, activity in the browser is blocked until the call succeeds or fails. This blocking
     * behavior avoids the difficult data integrity problems associated with asynchronous calls, at the expense of
     * application (indeed browser) liveness. Service methods take a single arguments that will marshalled using
     * JSON. If the method return type is void, undefined will be returned from the call. Otherwise, the return value
     * will be marshalled using JSON and returned to the browser.</p>
     * <h3>Asynchronous</h3>
     * <p>For an asynchronous call, you program will be notified on call completion by invoking one the the provided
     * callbacks. The callback object may have the following callback functions.</p>
     * <dl>
     * <dt>success</dt><dd>Called with the result of the call (undefined if the call has a return type of void).</dd>
     * <dt>failure</dt><dd>Called if the server returns a 4xx or 5xx result. An attempt will be made to JSON unmarshall
     *     an exception object from the returned text and pass the result as the argument to your failure callback.</dd>
     * <dt>precondition</dt><dd>Called to check if the preconditions for this invocation still hold when the result
     *      is received. This function returns <code>true</code> to indicate that the preconditions are still valid.
     *      If this function returns false, an optimistic lock failure is assumed and the <code>rollback</code>
     *      callback will be called to handle the failure.</dd>
     * <dt>rollback</dt><dd>Called if the precondition checks fail: essentially an optimistic lock failure.</dd>
     * </dl>
     * <p>The single argument to the Service method may be a function, in which case the result of calling the function
     *     is used as the call argument and an optimistic lock check will be made on a successful completion.</p>
     *
     * @public
     */
    addMethod: function( name )
    {
        var that = this;
        that[name] = function( callback, parameter )
        {
            var args = {};

            if( BHF.lang.isDefined( that.token ) )
                args.t = that.token;

            if( this.isInline( callback, arguments ) )
            {
                args.a = arguments[0];
                return that.invokeSync( name, args );
            }
            else
            {
                that.agumentWithDefaults( callback );
                args.a = parameter;
                return that.invoke( name, callback, args );
            }
        };
    },

    /**
     * Set the CSRF guard token. Optional.
     *
     * @param token CSRF guard token to be passed as the 't' parameter for all invocations
     */
    setToken: function( token )
    {
        this.token = token;
    },

    /**
     * @param policy Either <code>BHF.Service.DISPATCH_POLICY.CONCURRENT</code> or
     *     <code>BHF.Service.DISPATCH_POLICY.SERIAL</code>.
     * @public
     */
    setDispatchPolicy: function( policy )
    {
        this._setDispatchPolicy( policy );
    }
};
/**
 * Enumeration of dispatch policies.
 * @private
 */
BHF.Service.DISPATCH_POLICY =
{
    /**
     * Allow any number of concurrent requests on a <code>Service</code>. Pass to <code>setDispatchPolicy()</code>.
     * @public
     */
    CONCURRENT: {},
    /**
     * Allow only a single outstanding request at a time on a <code>Service</code>. Pending requests are queued FIFO.
     * Pass to <code>setDispatchPolicy()</code>.
     * @public
     */
    SERIAL: {}
};
/**
 * Predefined functions that you can pass as your rollback callback.
 */
BHF.Service.ROLLBACK_POLICY =
{
    /** Ignore the failed call. Pass as the <code>rollback</code> property of your callback. */
    IGNORE: function ( txn ) { },
    /** Retry the call using the current value for the call argument .Pass as the <code>rollback</code> property of your callback. */
    RETRY: function( txn )
    {
        txn.service.invoke( txn.name, txn.callback, txn.parameters );
    }
};
/**
 * Predefined functions that you can pass as your precondition callback.
 * @private
 */
BHF.Service.PRECONDITIONS =
{
    /**
     * The method argument must resolve now to the same value as before the call. They can only differ if
     * a function was passed instead of a value. Pass as the <code>precondition</code> property of your callback.
     * @public
     */
    SAME_ARG: function( txn )
    {
        var resolved = txn.service.resolve( txn.parameters );
        var k = 'a';

        return txn.resolved[k] === resolved[k];
    }
};
/**
 * Utility function to scrape input fields into the properties of the given object. This method will not handle
 * file uploads or plural name values.
 * @param ID of the form
 * @return An object with properties set per the form fields
 */
BHF.Service.gatherFields = function( formId, result )
{
    if( BHF.lang.isUndefined( result ) )
        result = {};

    var oForm;
    if(typeof formId == 'string'){
        // Determine if the argument is a form id or a form name.
        // Note form name usage is deprecated by supported
        // here for legacy reasons.
        oForm = (document.getElementById(formId) || document.forms[formId]);
    }
    else if(typeof formId == 'object'){
        // Treat argument as an HTML form object.
        oForm = formId;
    }
    else{
        return result;
    }

    var oElement, oName, oValue, oDisabled;
    var stuff = function( o, key, value )
    {
        var ps = key.split( /[\.\[]/ );
        var i, e, a;
        for( i = 0; i < ps.length - 1; i++ )
        {
            a = ps[i + 1].charAt( ps[i + 1].length - 1 ) == ']';
            e = ps[i];
            if( e.charAt( e.length - 1 ) == ']' )
                e = e.substring( 0, e.length - 1 );
            if( o[e] === undefined )
                o[e] = a ? [] : {};
            o = o[e];
        }
        e = ps[i];
        if( e.charAt( e.length - 1 ) == ']' )
            e = e.substring( 0, e.length - 1 );
        o[e]= value;
    }

    // Iterate over the form elements collection to construct the
    // label-value pairs.
    for( var i = 0; i < oForm.elements.length; i++ )
    {
        oElement    = oForm.elements[i];
        oDisabled   = oElement.disabled;
        oName       = oElement.name;
        oValue      = oElement.value;

        // Do not submit fields that are disabled or
        // do not have a name attribute value.
        if(!oDisabled && oName)
        {
            switch(oElement.type)
            {
                case 'select-one':
                case 'select-multiple':
                    for(var j=0; j<oElement.options.length; j++)
                    {
                        if(oElement.options[j].selected)
                        {
                            if( window.ActiveXObject )
                            {
                                stuff( result, oName, oElement.options[j].attributes['value'].specified?oElement.options[j].value:oElement.options[j].text );
                            }
                            else
                            {
                                stuff( result, oName,  oElement.options[j].hasAttribute('value')?oElement.options[j].value:oElement.options[j].text );
                            }
                        }
                    }
                    break;
                case 'radio':
                    if( oElement.checked )
                    {
                        stuff( result, oName, oValue );
                    }
                    break;
                case 'checkbox':
                    stuff( result, oName, oElement.checked );
                    break;
                case 'file':
                    // stub case as XMLHttpRequest will only send the file path as a string.
                case undefined:
                    // stub case for fieldset element which returns undefined.
                case 'reset':
                    // stub case for input type reset button.
                case 'button':
                    // stub case for input type button elements.
                case 'submit':
                    break;
                default:
                    stuff( result, oName, oValue );
            }
        }
    }

    return result;
};
/**
 * Utility function to populate input fields from the properties of the given object. This method will not handle
 * file uploads or plural name values.
 * @param ID of the form
 * @param o The object with the form properties
 */
BHF.Service.scatterFields = function( formId, o )
{
    var oForm;
    if( typeof formId === 'string' ) {
        // Determine if the argument is a form id or a form name.
        // Note form name usage is deprecated by supported
        // here for legacy reasons.
        oForm = (document.getElementById(formId) || document.forms[formId]);
    }
    else if( typeof formId === 'object' ){
        // Treat argument as an HTML form object.
        oForm = formId;
    }
    else {
        return;
    }

    var oElement, oName, oValue, oDisabled, rValue;
    var extract = function( o, key )
    {
        var ps = key.split( /[\.\[]/ );
        var i, e, a;
        for( i = 0; i < ps.length - 1; i++ )
        {
            a = ps[i + 1].charAt( ps[i + 1].length - 1 ) == ']';
            e = ps[i];
            if( e.charAt( e.length - 1 ) == ']' )
                e = e.substring( 0, e.length - 1 );
            if( o[e] === undefined )
                o[e] = a ? [] : {};
            o = o[e];
        }
        e = ps[i];
        if( e.charAt( e.length - 1 ) == ']' )
            e = e.substring( 0, e.length - 1 );
        return o[e];
    }

    // Iterate over the form elements collection to construct the
    // label-value pairs.
    for( var i = 0; i < oForm.elements.length; i++ )
    {
        oElement    = oForm.elements[i];
        oName       = oElement.name;
        oValue      = oElement.value;
        rValue      = oName ? extract( o, oName ) : null;

        // Do not submit fields that are disabled or
        // do not have a name attribute value.
        if( rValue )
        {
            switch( oElement.type )
            {
                case 'select-one':
                case 'select-multiple':
                    for( var j = 0; j < oElement.options.length; j++ )
                    {
                        if( window.ActiveXObject )
                        {
                            oValue = oElement.options[j].attributes['value'].specified?oElement.options[j].value:oElement.options[j].text;
                        }
                        else
                        {
                            oValue = oElement.options[j].hasAttribute('value')?oElement.options[j].value:oElement.options[j].text;
                        }
                        oElement.options[j].selected = oValue == rValue;
                    }
                    break;
                case 'radio':
                    oElement.checked = oValue == rValue;
                    break;
                case 'checkbox':
                    oElement.checked = rValue;
                    break;
                case 'file':
                    // stub case as XMLHttpRequest will only send the file path as a string.
                case undefined:
                    // stub case for fieldset element which returns undefined.
                case 'reset':
                    // stub case for input type reset button.
                case 'button':
                    // stub case for input type button elements.
                case 'submit':
                    break;
                default:
                    oElement.value = rValue;
            }
        }
    }
};
/** Fixes IE bug when setting window.location with a relative URL */
BHF.fixRelativeURL = function( url )
{
    var b = document.getElementsByTagName('base');
    if( b && b[0] && b[0].href )
    {
        if( b[0].href.substr(b[0].href.length-1) == '/' && url.charAt(0) == '/' )
            url = url.substr(1);
        url = b[0].href + url;
    }
    return url;
}
