/**
 * Package inviso.ajax
 *
 * This file contains functions and classes to perform AJAX requests on inviso.
 *
 * Usage example:
 * 
 * inviso_ajax()
 *   .block('block-id')
 *     .action('update-some-div')
 *       .set('parameter-a', '1234')
 *       .set('parameter-b', 'value')
 *     .response('response-template')
 *     
 *   .block('block-id')
 *     .action('widget', 'action')
 *       .set('name', 'value')
 *     .response('another-template.tpl')
 *     
 *   .send();
 *
 * @package inviso.ajax
 * @author  Lesley Wevers
 * 
 * @update Updated to work on prototype
 * @author Ronald Volgers
 */

/**
 * Performs an inviso call
 *
 * @param   string  block_id    The id of the block from which this function is called
 * @param   string  action      The action which has to be called
 * @param   object  parameters  The parameters for the action
 */
function inviso_call(block_id, action, parameters, options) {
    inviso_ajax_block(block_id).action(action, parameters, options).send();
}

/**
 * Builds an inviso ajax request object
 *
 * @return  InvisoAjaxRequest   A new InvisoAjaxRequest object
 */
function inviso_ajax() {
    return new InvisoAjaxRequest();
}

/**
 * Builds an inviso request with a block context
 *
 * @return  InvisoAjaxEntity    A block context
 */
function inviso_ajax_block(id) {
    request = new InvisoAjaxRequest();
    
    return request.block(id);
}

/**
 * Builds an inviso request with a page context
 *
 * @return  InvisoAjaxEntity    A page context
 */
function inviso_ajax_page(url) {
    request = new InvisoAjaxRequest();

    return request.page(url);
}

/**
 * Used to share a ajax request between multiple widgets. You can use the widgets events framework to coordinate between the widgets.
 * Use just like inviso_ajax(). 
 * The request will be sent when send() has been called once for every inviso_shared_ajax() call.
 */
function inviso_shared_ajax() {

	// Private subclass of InvisoAjaxRequest that doesn't send anything until inviso_shared_ajax.nestingLevel==0
	var InvisoSharedAjaxRequest = Class.create(InvisoAjaxRequest, {
		send: function($super, options) {
			if (--inviso_shared_ajax.nestingLevel == 0) {
				$super(options);
				inviso_shared_ajax.ajax = undefined; // Release request object
			}
		}
	});

	// Create a new ajax request object if none exists
	if (inviso_shared_ajax.ajax == undefined) {
		inviso_shared_ajax.ajax = new InvisoSharedAjaxRequest();
		inviso_shared_ajax.nestingLevel = 0;
	}
	
	// Increment reference count
	inviso_shared_ajax.nestingLevel++;

	return inviso_shared_ajax.ajax;
}

/**
 * InvisoAjaxRequest class constructor
 */
InvisoAjaxRequest = Class.create({
  initialize: function() {
	this.callbacks = [];
    this.blocks = [];
	this.sent = false;
  },
  
  callback: function(cb) {
	this.callbacks.push(cb);
	return this;
  },
    
    /**
     * Adds a block context
     *
     * @param   int                 id  The id of the block
     * @return  InvisoAjaxContext       A block context
     */
    block: function(id) {
        block = new InvisoAjaxBlock(this, id);
        this.blocks.push(block);
        return block;
    },
    
    /**
     * Performs the AJAX request
     *
     * @return  mixed   Result of xajax.call
     */
    send: function(options) {
	
		// Sanity checks
		if (this.sent) {
			throw new Error('Attempting to re-send a ajax request.');
		}
		this.sent = true;
		
		// Don't do an empty ajax request. Just do the callbacks and exit.
		if (this.blocks.length == 0) {
			$A(this.callbacks).invoke('call');
			return;
		}
		
		// Initialize options
		if (options == undefined) { options = {}; }
	
		// Gather block requests 
        var block_requests = $A(this.blocks).collect(
			function (a) { return a.build(); }
		);
        
		// Handle requests for callbacks
		if (this.callbacks.length > 0) {
			
			// utility function that combines multiple argumentless functions into one
			var compose = function (fns) {
				var tmp = $A(fns);
				return tmp.invoke.bind(tmp, 'call');
			}
			
			// Initialize callbacks
			if (options.callback == undefined) { options.callback = {}; }
			
			// Preserve existing onComplete handler, if any
			if (options.callback.onComplete != undefined) {
				this.callbacks.push(options.callback.onComplete);
			}
			
			// Set the appropriate option (ytw_call passes this on to xajax)
			options.callback.onComplete = compose(this.callbacks);
		}

		var arguments = [{ 'blocks' : block_requests }];
		
        return ytw_call('modInviso', 'handle_widget', arguments, options);
    }
});

/**
 * InvisoAjaxEntity class constructor
 *
 * @param   mixed   parent  The parent object
 */
InvisoAjaxBlock = Class.create({
	initialize: function(parent, block_id) {
		this.block_id = block_id;
		this.parent    = parent;
		this.actions   = [];
		this.responses = [];
	},
	
	/**
	 * Call the given function when the request completes (succesfully).
	 * During the callback 'this' is bound to the most recently specified 'block' object in the current request,
	 */
	callback: function(cb) {
		this.parent.callback(cb.bind($inviso(this.block_id)));
		
		return this;
	},
    
    /**
     * Requests an action
     *
     * @param   string              action  The name of the action to request
     * @return  InvisoAjaxAction            A new InvisoAjaxAction object
     */
    action: function(name, parameters_) {
	
        parameters = new InvisoAjaxParameters(this, name); 
        
        // If parameters argument is given add them to the parameters object
        if(parameters_ != undefined) {
            for(parameter_name in parameters_) {
                parameters.set(parameter_name, parameters_[parameter_name]);
            }
        }
        
        this.actions.push([name, parameters]);
        
        return parameters;
    },
    
    /**
     * Requests a response template
     *
     * @param   string              template    The name of the response template
     * @return  InvisoAjaxRequest               This object
     */
    response: function(response) {
        this.responses.push(response);
        
        return this;
    },
    
    /**
     * Redirects to parent.widget
     */
    block: function(name) {
        return this.parent.block(name);
    },
        
    /**
     * Redirects to parent.send
     */
    send: function(options) {
        return this.parent.send(options);
    },
    
    /**
     * Builds the ajax call parameters for this entity
     */
    build: function() {
        actions = {};
        
        for(var i = 0; i < this.actions.length; i++) {
            actions[ this.actions[i][0] ] = this.actions[i][1].get_parameters();
        }
        
        return {'block_id': this.block_id, 'action' : actions, 'response' : this.responses};
    }
});

/**
 * InvisoAjaxParameters class constructor
 *
 * @param   mixed   parent  The parent of this object
 */
InvisoAjaxParameters = Class.create({
  initialize: function(parent) {
    this.parent = parent;
    
    this.parameters  = {};
  },
    /**
     * Sets a parameter
     * 
     * @param   string              name    The name of the parameter
     * @param   string              value   The value of the parameter
     * @return  InvisoAjaxAction            This object
     */
    set: function(name, value) {
        this.parameters[name] = value;
        
        return this;
    },

    /**
     * Redirects to parent.action
     */
    action: function(action) {
        return this.parent.action(action);
    },
    
    /**
     * Redirects to parent.response
     */
    response: function(template) {
        return this.parent.response(template);
    },
    
    /**
     * Redirects to parent.widget
     */
    block: function(name) {
        return this.parent.block(name);
    },

    /**
     * Redirects to parent.send
     */
    send: function(options) {
        return this.parent.send(options);
    },
	
	/**
	 * Redirects to parent.callback
	 */
	callback: function(code) {
		return this.parent.callback(code);
	},
    
    /**
     * 
     */
    get_parameters: function() {
        return this.parameters;
    }
});

