/** 
 * Generic BBOP manager for dealing with basic generic REST calls.
 * This specific one is designed to be overridden by its subclasses.
 * This one pretty much just uses its incoming resource string as the data.
 * Mostly for testing purposes.
 * 
 * Both a <bbop-rest-response> (or clean error data) and the manager
 * itself (this as anchor) should be passed to the callbacks.
 *
 * @module bbop-rest-manager
 */

// For base.
var us = require('underscore');
var each = us.each;
var bbop = require('bbop-core');
var registry = require('bbop-registry');

// For engines.
var Q = require('q');
var querystring = require('querystring');
var jQuery = require('jquery');
var sync_request = require('sync-request');

///
/// Base class.
///

/**
 * Contructor for the REST manager.
 * 
 * See also: module:bbop-registry
 * 
 * @constructor
 * @param {Object} response_parser - the response handler class to use for each call
 * @returns {Object} rest manager object
 */
function manager_base(response_handler){
    registry.call(this, ['success', 'error']);
    this._is_a = 'bbop-rest-manager.base';

    // Get a good self-reference point.
    var anchor = this;

    // Per-manager logger.
    this._logger = new bbop.logger(this._is_a);
    //this._logger.DEBUG = true;
    this._logger.DEBUG = false;
    function ll(str){ anchor._logger.kvetch(str); }

    // Handler instance.
    this._response_handler = response_handler;

    // The URL to query.
    this._qurl = null;

    // The argument payload to deliver to the URL.
    this._qpayload = {};

    // The way to do the above.
    this._qmethod = 'GET';

    // Whether or not to prevent ajax events from going.
    // This may not be usable, or applicable, to all backends.
    this._safety = false;

    /**
     * Turn on or off the verbose messages. Uses <bbop.logger>, so
     * they should come out everywhere.
     * 
     * @param {Boolean} [p] - true or false for debugging
     * @returns {Boolean} the current state of debugging
     */
    this.debug = function(p){
	if( p === true || p === false ){
	    this._logger.DEBUG = p;
	    // TODO: add debug parameter a la include_highlighting
	}
	return this._logger.DEBUG;
    };

    // The main callback function called after a successful AJAX call in
    // the update function.
    this._run_success_callbacks = function(in_data){
	ll('run success callbacks...');
	//var response = anchor.(in_data);
	var response = new anchor._response_handler(in_data);
	anchor.apply_callbacks('success', [response, anchor]);
    };

    // This set is called when we run into a problem.
    this._run_error_callbacks = function(in_data){
	ll('run error callbacks...');
	var response = new anchor._response_handler(in_data);
	anchor.apply_callbacks('error', [response, anchor]);
    };

    // Ensure the necessary 
    this._ensure_arguments = function (url, payload, method){
	ll('ensure arguments...');
	
	// Allow default settings to be set at the moment.
	if( typeof(url) !== 'undefined' ){ this.resource(url); }
	if( typeof(payload) !== 'undefined' ){ this.payload(payload); }
	if( typeof(method) !== 'undefined' ){ this.method(method); }
	
	// Bail if no good resource to try.
	if( ! this.resource() ){
	    throw new Error('must have resource defined');
	}
    };

    // Apply the callbacks by the status of the response.
    this._apply_callbacks_by_response = function (response){
	ll('apply callbacks by response...');

	if( response && response.okay() ){
	    anchor.apply_callbacks('success', [response, anchor]);
	}else{
	    anchor.apply_callbacks('error', [response, anchor]);
	}
    };

    /**
     * The base target URL for our operations.
     * 
     * @param {String} [in_url] - update resource target with string
     * @returns {String|null} the url as string (or null)
     */
    this.resource = function(in_url){
	ll('resource called with: ' + in_url);

	if( typeof(in_url) !== 'undefined' && 
	    bbop.what_is(in_url) === 'string' ){
	    anchor._qurl = in_url;
	}
	return anchor._qurl;
    };

    /**
     * The information to deliver to the resource.
     * 
     * @param {Object} [payload] - update payload information
     * @returns {Object|null} a copy of the current payload
     */
    this.payload = function(payload){
	ll('payload called with: ' + payload);

	if( bbop.is_defined(payload) && 
	    bbop.what_is(payload) === 'object' ){
	    anchor._qpayload = payload;
	}
	return bbop.clone(anchor._qpayload);
    };

    /**
     * The method to use to get the resource, as a string.
     * 
     * @param {String} [method] - update aquisition method with string
     * @returns {String|null} the string or null
     */
    this.method = function(method){
	ll('method called with: ' + method);

	if( bbop.is_defined(method) && 
	    bbop.what_is(method) === 'string' ){
	    anchor._qmethod = method;
	}
	return anchor._qmethod;
    };
}
bbop.extend(manager_base, registry);

///
/// Overridables.
///

/**
 * Output writer for this object/class.
 * See the documentation in <core.js> on <dump> and <to_string>.
 * 
 * @returns {String}  string
 */
manager_base.prototype.to_string = function(){
    return '[' + this._is_a + ']';
};

/**
 * Assemble the resource and arguments into a URL string.
 * 
 * May not be appropriate for all subclasses or commands (and probably
 * only useful in the context of GET calls, etc.). Often used as a
 * helper, etc.
 * 
 * Also see: <get_query_url>
 * 
 * @returns {String} url string
 */
manager_base.prototype.assemble = function(){

    // Conditional merging of the remaining variant parts.
    var qurl = this.resource();
    if( ! bbop.is_empty(this.payload()) ){
	var asm = bbop.get_assemble(this.payload());
	qurl = qurl + '?' + asm;
    }
    return qurl;
};

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 * 
 * This model class always returns true, with set messages; the
 * "payload" is fed as the argument into the response handler.
 * 
 * What we're aiming for is a system that:
 *  - runs callbacks (in order: success, error, return)
 *  - return response
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} response (given the incoming payload)
 */
manager_base.prototype.fetch = function(url, payload, method){

    var anchor = this;
    anchor._logger.kvetch('called fetch');

    this._ensure_arguments(url, payload, method);

    // This is an empty "sync" example, so just return the empty and
    // see.
    var response = new this._response_handler(this.payload());
    response.okay(true);
    response.message('empty');
    response.message_type('success');
    
    // Run through the callbacks--naturally always "success" in our
    // case.
    this._apply_callbacks_by_response(response);

    return response;
};

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 * 
 * This model class always returns true, with set messages; the
 * "payload" is fed as the argument into the response handler.
 * 
 * What we're aiming for is a system that:
 *  - runs callbacks (in order: success, error, return)
 *  - return promise (delivering response)
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} promise for the processed response subclass
 */
manager_base.prototype.start = function(url, payload, method){

    var anchor = this;
    this._ensure_arguments(url, payload, method);

    // No actual async here, but do anyways.
    var deferred = Q.defer();

    // This is an empty "sync" example, so just return the empty and
    // see.
    var response = new this._response_handler(this.payload());
    response.okay(true);
    response.message('empty');
    response.message_type('success');
    
    // Run through the callbacks--naturally always "success" in our
    // case.
    this._apply_callbacks_by_response(response);

    deferred.resolve(response);

    return deferred.promise;
};

///
/// Node async engine.
///

/**
 * Contructor for the REST query manager; NodeJS-style.
 * 
 * This is an asynchronous engine, so while both fetch and start will
 * run the callbacks, fetch will return null while start returns a
 * promise for the eventual result. Using the promise is entirely
 * optional--the main method is still considered to be the callbacks.
 * 
 * NodeJS BBOP manager for dealing with remote calls. Remember,
 * this is actually a "subclass" of <bbop.rest.manager>.
 * 
 * See also: {module:bbop-rest-manager#manager}
 *
 * @constructor
 * @param {Object} response_handler
 * @returns {manager_node}
 */
var manager_node = function(response_handler){
    manager_base.call(this, response_handler);
    this._is_a = 'bbop-rest-manager.node';

    // Grab an http client.
    this._http_client = require('http');
    this._url_parser = require('url');
};
bbop.extend(manager_node, manager_base);

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 *
 * Runs callbacks, returns null.
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {null} returns null
 */
manager_node.prototype.fetch = function(url, payload, method){

    var anchor = this;
    anchor._logger.kvetch('called fetch');

    // Pass off.
    this.start(url, payload, method);

    return null;
};

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 * 
 * What we're aiming for is a system that:
 *  - runs callbacks (in order: success, error, return)
 *  - return promise (delivering response)
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} promise for the processed response subclass
 */
manager_node.prototype.start = function(url, payload, method){

    var anchor = this;

    this._ensure_arguments(url, payload, method);

    // Our eventual promise.
    var deferred = Q.defer();

    // What to do if an error is triggered.
    function on_error(e) {
	console.log('problem with request: ' + e.message);
	var response = new anchor._response_handler(null);
	response.okay(false);
	response.message(e.message);
	response.message_type('error');
	anchor.apply_callbacks('error', [response, anchor]);
	deferred.resolve(response);
    }

    // Two things to do here: 1) collect data and 2) what to do with
    // it when we're done (create response).
    function on_connect(res){
	//console.log('STATUS: ' + res.statusCode);
	//console.log('HEADERS: ' + JSON.stringify(res.headers));
	res.setEncoding('utf8');
	var raw_data = '';
	res.on('data', function (chunk) {
	    //console.log('BODY: ' + chunk);
	    raw_data = raw_data + chunk;
	});
	// Throw to .
	res.on('end', function () {
	    //console.log('END with: ' + raw_data);
	    var response = new anchor._response_handler(raw_data);
	    if( response && response.okay() ){
		anchor.apply_callbacks('success', [response, anchor]);
		deferred.resolve(response);
	    }else{
		// Make sure that there is something there to
		// hold on to.
		if( ! response ){
		    response = new anchor._response_handler(null);
		    response.okay(false);
		    response.message_type('error');
		    response.message('null response');
		}else{
		    response.message_type('error');
		    response.message('bad response');
		}
		anchor.apply_callbacks('error', [response, anchor]);
		deferred.resolve(response);
	    }
	});
    }

    // http://nodejs.org/api/url.html
    var purl = anchor._url_parser.parse(anchor.resource());
    var req_opts = {
    	//'hostname': anchor.resource(),
    	//'path': '/amigo/term/GO:0022008/json',
	//'port': 80,
	'method': anchor.method()
    };
    // Tranfer the interesting bit over.
    each(['protocol', 'hostname', 'port', 'path'], function(purl_prop){
    	if( purl[purl_prop] ){
    	    req_opts[purl_prop] = purl[purl_prop];
    	}
    });

    // Add any payload if it exists. On an empty payload, post_data
    // will still be '', so no real harm done.
    var post_data = querystring.stringify(anchor.payload());
    req_opts['headers'] = {
	'Content-Type': 'application/x-www-form-urlencoded',
	'Content-Length': post_data.length
    };

    //console.log('req_opts', req_opts);

    var req = anchor._http_client.request(req_opts, on_connect);

    // Oh yeah, add the error responder.
    req.on('error', on_error);
    
    // Write data to request body.
    req.write(post_data);
    req.end();
    
    return deferred.promise;
};

///
/// Node sync engine.
///

/**
 * Contructor for the REST query manager--synchronous in node.
 * 
 * This is an synchronous engine, so while both fetch and start will
 * run the callbacks, fetch will return a response while start returns
 * an instantly resolvable promise. Using the response results is
 * entirely optional--the main method is still considered to be the
 * callbacks.
 * 
 * See also: <bbop.rest.manager>
 * 
 * @constructor
 * @param {Object} response_handler
 * @returns {manager_node_sync}
  */
var manager_node_sync = function(response_handler){
    manager_base.call(this, response_handler);
    this._is_a = 'bbop-rest-manager.node_sync';
};
bbop.extend(manager_node_sync, manager_base);

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} returns response
 */
manager_node_sync.prototype.fetch = function(url, payload, method){
    var anchor = this;

    this._ensure_arguments(url, payload, method);

    // Grab the data from the server.
    var res = null;
    try {
	res = sync_request(anchor.method(), anchor.resource(), anchor.payload());
    }
    catch(e){
	console.log('ERROR in node_sync call, will try to recover');
    }
    
    //
    var raw_str = null;
    if( res && res.statusCode < 400 ){
	raw_str = res.getBody().toString();
    }else if( res && res.body ){
	raw_str = res.body.toString();
    }else{
	//
    }

    // Process and pick the right callback group accordingly.
    var response = null;
    if( raw_str && raw_str !== '' && res.statusCode < 400 ){
	response = new anchor._response_handler(raw_str);
	this.apply_callbacks('success', [response, anchor]);
    }else{
	response = new anchor._response_handler(null);
	this.apply_callbacks('error', [response, anchor]);
	//throw new Error('explody');
    }

    return response;
};

/**
 * This is the synchronous data getter for Node (and technically the
 * browser, but never never do that)--probably your best bet right now
 * for scripting.
 * 
 * Works as fetch, except returns an (already resolved) promise.
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} returns promise
 */
manager_node_sync.prototype.start = function(url, payload, method){
    var anchor = this;

    var response = anchor.fetch(url, payload, method);

    // .
    var deferred = Q.defer();
    deferred.resolve(response);
    return deferred.promise;
};

///
/// jQuery engine.
///

/**
 * Contructor for the jQuery REST manager
 * 
 * jQuery BBOP manager for dealing with actual ajax calls. Remember,
 * this is actually a "subclass" of {bbop-rest-manager}.
 * 
 * Use <use_jsonp> is you are working against a JSONP service instead
 * of a non-cross-site JSON service.
 * 
 * See also:
 *  <bbop.rest.manager>
 *
 * @constructor
 * @param {Object} response_handler
 * @returns {manager_node_sync}
 */
var manager_jquery = function(response_handler){
    manager_base.call(this, response_handler);
    this._is_a = 'bbop-rest-manager.jquery';

    this._use_jsonp = false;
    this._jsonp_callback = 'json.wrf';
    this._headers = null;
    
    // Track down and try jQuery.
    var anchor = this;
    //anchor.JQ = new bbop.rest.manager.jquery_faux_ajax();
    try{ // some interpreters might not like this kind of probing
    	if( typeof(jQuery) !== 'undefined' ){
    	    anchor.JQ = jQuery;
    	    //anchor.JQ = jQuery.noConflict();
    	}
    }catch (x){
	throw new Error('unable to find "jQuery" in the environment');
    }
};
bbop.extend(manager_jquery, manager_base);

/**
 * Set the jQuery engine to use JSONP handling instead of the default
 * JSON. If set, the callback function to use will be given my the
 * argument "json.wrf" (like Solr), so consider that special.
 * 
 * @param {Boolean} [use_p] - external setter for 
 * @returns {Boolean} boolean
 */
manager_jquery.prototype.use_jsonp = function(use_p){
    var anchor = this;
    if( typeof(use_p) !== 'undefined' ){
	if( use_p === true || use_p === false ){
	    anchor._use_jsonp = use_p;
	}
    }
    return anchor._use_jsonp;
};

/**
 * Get/set the jQuery jsonp callback string to something other than
 * "json.wrf".
 * 
 * @param {String} [cstring] - setter string
 * @returns {String} string
 */
manager_jquery.prototype.jsonp_callback = function(cstring){
    var anchor = this;
    if( typeof(cstring) !== 'undefined'  ){
	anchor._jsonp_callback = cstring;
    }
    return anchor._jsonp_callback;
};

/**
 * Try and control the server with the headers.
 * 
 * @param {Object} [header_set] - hash of headers; jQuery internal default
 * @returns {Object} hash of headers
 */
manager_jquery.prototype.headers = function(header_set){
    var anchor = this;
    if( typeof(header_set) !== 'undefined' ){
	anchor._headers = header_set;
    }
    return anchor._headers;
};

/**
 * It should combine the URL, payload, and method in the ways
 * appropriate to the subclass engine.
 *
 * Runs callbacks, returns null.
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {null} returns null
 */
manager_jquery.prototype.fetch = function(url, payload, method){

    var anchor = this;
    anchor._logger.kvetch('called fetch');

    // Pass off.
    anchor.start(url, payload, method);

    return null;
};

/**
 * See the documentation in <manager.js> on update to get more
 * of the story. This override function adds functionality for
 * jQuery.
 * 
 * @param {String} [url] - update resource target with string
 * @param {Object} [payload] - object to represent arguments
 * @param {String} [method - GET, POST, etc.
 * @returns {Object} promise for the processed response subclass
 */
manager_jquery.prototype.start = function(url, payload, method){

    var anchor = this;
    
    this._ensure_arguments(url, payload, method);

    // Our eventual promise.
    var deferred = Q.defer();

    // URL and payload (jQuery will just append as arg for GETs).
    var qurl = anchor.resource();
    var pl = anchor.payload();

    // The base jQuery Ajax args we need with the setup we have.
    var jq_vars = {
    	url: qurl,
	data: pl,
    	dataType: 'json',
	headers: {
	    "Content-Type": "application/javascript",
	    "Accept": "application/javascript"
	},
    	type: anchor.method()
    };

    // If we're going to use JSONP instead of the defaults, set that now.
    if( anchor.use_jsonp() ){
	jq_vars['dataType'] = 'jsonp';
	jq_vars['jsonp'] = anchor._jsonp_callback;
    }
    if( anchor.headers() ){
    	jq_vars['headers'] = anchor.headers();
    }

    // What to do if an error is triggered.
    // Remember that with jQuery, when using JSONP, there is no error.
    function on_error(xhr, status, error) {
	var response = new anchor._response_handler(null);
	response.okay(false);
	response.message(error);
	response.message_type(status);
	anchor.apply_callbacks('error', [response, anchor]);
	deferred.resolve(response);
    }

    function on_success(raw_data, status, xhr){
	var response = new anchor._response_handler(raw_data);
	if( response && response.okay() ){
	    anchor.apply_callbacks('success', [response, anchor]);
	    deferred.resolve(response);
	}else{
	    // Make sure that there is something there to
	    // hold on to.
	    if( ! response ){
		response = new anchor._response_handler(null);
		response.okay(false);
		response.message_type(status);
		response.message('null response');
	    }else{
		response.message_type(status);
		response.message('bad response');
	    }
	    //anchor.apply_callbacks('error', [response, anchor]);
	    //anchor.apply_callbacks('error', [raw_data, anchor]);
	    anchor.apply_callbacks('error', [response, anchor]);
	    deferred.resolve(response);
	}
    }

    // Setup JSONP for Solr and jQuery ajax-specific parameters.
    jq_vars['success'] = on_success;
    jq_vars['error'] = on_error;
    //done: _callback_type_decider, // decide & run search or reset
    //fail: _run_error_callbacks, // run error callbacks
    //always: function(){} // do I need this?
    anchor.JQ.ajax(jq_vars);

    return deferred.promise;
};

///
/// Exportable body.
///

module.exports = {

    "base" : manager_base,
    "node" : manager_node,
    "node_sync" : manager_node_sync,
    "jquery" : manager_jquery

};