var http = require('follow-redirects').http,
	https = require('follow-redirects').https,
	parseString = require('xml2js').parseString,
	urlParser = require('url'),
	util = require("util"),
	events = require("events"),
	zlib = require("zlib"),
	node_debug = require("debug")("NRC");

exports.Client = function (options){
	var self = this;
	var connectManager = new ConnectManager();
	//declare util constants
	var CONSTANTS={
		HEADER_CONTENT_LENGTH:"Content-Length"
	};


	self.options = options || {},
	self.useProxy = (self.options.proxy || false)?true:false,
    self.useProxyTunnel = (!self.useProxy || self.options.proxy.tunnel===undefined)?false:self.options.proxy.tunnel,
	self.proxy = self.options.proxy,
	self.connection = self.options.connection || {},
	self.mimetypes = self.options.mimetypes || {},
	self.requestConfig = self.options.requestConfig || {},
	self.responseConfig = self.options.responseConfig || {};

	this.methods={};

	// Client Request to be passed to ConnectManager and returned
	// for each REST method invocation
	var ClientRequest =function(){
		events.EventEmitter.call(this);
	};


	util.inherits(ClientRequest, events.EventEmitter);
	

	ClientRequest.prototype.end = function(){
		if(this._httpRequest) {
			this._httpRequest.end();
		}
	};

	ClientRequest.prototype.setHttpRequest=function(req){
		this._httpRequest = req;
	};



	var Util = {
	 createProxyPath:function(url){
	    var result = url.host;
		 // check url protocol to set path in request options
		 if (url.protocol === "https:"){
			// port is set, leave it, otherwise use default https 443
			result = (url.host.indexOf(":") == -1?url.hostname + ":443":url.host);
		}

		return result;
	  },
	 createProxyHeaders:function(url){
		var result ={};
		// if proxy requires authentication, create Proxy-Authorization headers
		if (self.proxy.user && self.proxy.password){
			result["Proxy-Authorization"] = "Basic " + new Buffer([self.proxy.user,self.proxy.password].join(":")).toString("base64");
		}
		// no tunnel proxy connection, we add the host to the headers
		if(!self.useProxyTunnel)
			result["host"] = url.host;

		return result;
	  },
	  createConnectOptions:function(connectURL, connectMethod){
			debug("connect URL = ", connectURL);
			var url = urlParser.parse(connectURL),
				path,
				result={},
				protocol = url.protocol.indexOf(":") == -1?url.protocol:url.protocol.substring(0,url.protocol.indexOf(":")),
				defaultPort = protocol === 'http'?80:443;

		    result ={
				host: url.host.indexOf(":") == -1?url.host:url.host.substring(0,url.host.indexOf(":")),
				port: url.port === undefined?defaultPort:url.port,
				path: url.path,
				protocol:protocol,
				href:url.href
		     };

		     if (self.useProxy) result.agent = false; // cannot use default agent in proxy mode

		     if (self.options.user && self.options.password){
				result.auth = [self.options.user,self.options.password].join(":");

			 } else if (self.options.user && !self.options.password){
			 	// some sites only needs user with no password to authenticate
			 	result.auth = self.options.user + ":";
			 }	


			// configure proxy connection to establish a tunnel
			if (self.useProxy){

				result.proxy ={
					host: self.proxy.host,
					port: self.proxy.port,
					method: self.useProxyTunnel?'CONNECT':connectMethod,//if proxy tunnel use 'CONNECT' method, else get method from request,
					path: self.useProxyTunnel?this.createProxyPath(url):connectURL, // if proxy tunnel set proxy path else get request path,
					headers: this.createProxyHeaders(url) // createProxyHeaders add correct headers depending of proxy connection type
				    };
			}

            if(self.connection && typeof self.connection === 'object'){
                for(var option in self.connection){
                        result[option] = self.connection[option];
                }
            }

            // don't use tunnel to connect to proxy, direct request
            // and delete proxy options
            if (!self.useProxyTunnel){
				for (option in result.proxy){
					result[option] = result.proxy[option];
				}

				delete result.proxy;
            }

            // add general request and response config to connect options

            result.requestConfig = self.requestConfig;
            result.responseConfig = self.responseConfig;


			return result;
		},
		decodeQueryFromURL: function(connectURL){
			var url = urlParser.parse(connectURL),
					  query = url.query.substring(1).split("&"),
					  keyValue,
					  result={};

			// create decoded args from key value elements in query+
			for (var i=0;i<query.length;i++){
				keyValue = query[i].split("=");
				result[keyValue[0]] = decodeURIComponent(keyValue[1]);
			}

			return result;

		},
		encodeQueryFromArgs: function(args){
			var result="?", counter = 1;
			// create enconded URL from args
			for (var key in args) {
				var keyValue = "";
				if ( args[key] instanceof Array )  { 
					/* 
					 * We are dealing with an array in the query string  ?key=Value0&key=Value1 
					 * That a REST application translates into key=[Value0, Value1]
					 */
					for ( var ii=0, sizeArray = args[key].length; ii < sizeArray; ii++ ) {
						result = result.concat((counter > 1 ? "&": "") + key + "=" + encodeURIComponent(args[key][ii]));
						counter++;
					}
				} else { //No array, just a single &key=value
				   keyValue = key + "=" + encodeURIComponent(args[key]);
				   result = result.concat((counter > 1 ? "&":"") + keyValue);
				}
				
				counter++;
			}

			return result;
		},
		parsePathParameters:function(args,url){
			var result = url;
			if (!args || !args.path) return url;

			for (var placeholder in args.path){
				var regex = new RegExp("\\$\\{" + placeholder + "\\}","i");
				result = result.replace(regex,args.path[placeholder]);
				
			}
			
			return result;

		},
		overrideClientConfig:function(connectOptions,methodOptions){
			function validateReqResOptions(reqResOption){
				return (reqResOption && typeof reqResOption === 'object');
			}
			// check if we have particular request or response config set on this method invocation
		    // and override general request/response config
		    if (validateReqResOptions(methodOptions.requestConfig)){
				util._extend(connectOptions.requestConfig,methodOptions.requestConfig);
		     }

		    if (validateReqResOptions(methodOptions.responseConfig)){
				util._extend(connectOptions.responseConfig,methodOptions.responseConfig);
		     }


		},
		connect : function(method, url, args, callback, clientRequest){
			// configure connect options based on url parameter parse
			var options = this.createConnectOptions(this.parsePathParameters(args,url), method);
			debug("options pre connect",options);
			options.method = method,
			clientRequest.href=options.href,
			options.clientRequest = clientRequest,
			options.headers= options.headers || {};
			
			debug("args = ", args);
			debug("args.data = ", args !== undefined?args.data:undefined);
			// no args passed
			if (typeof args === 'function'){
				callback = args;
				//add Content-length to POST/PUT/DELETE/PATCH methods
				if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH'){					
					options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = 0;					 	
				}
			} else if (typeof args === 'object') {
				// add headers and POST/PUT/DELETE/PATCH data to connect options to be passed
				// with request, but without deleting other headers like non-tunnel proxy headers
				if (args.headers){
					for (var headerName in args.headers){
						if (args.headers.hasOwnProperty(headerName)) {
    							options.headers[headerName] = args.headers[headerName];	
						}
					}
							
				}
				
				//always set Content-length header if not set previously
				//set Content lentgh for some servers to work (nginx, apache) 
				if (args.data !== undefined && !options.headers.hasOwnProperty(CONSTANTS.HEADER_CONTENT_LENGTH)){
					options.data = args.data;					
					options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = Buffer.byteLength((typeof args.data === 'string' ? args.data:JSON.stringify(args.data)), 'utf8');
				}else{
					options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = 0;
				}
		        // we have args, go and check if we have parameters
		        if (args.parameters && Object.keys(args.parameters).length > 0){
		          // validate URL consistency, and fix it
		          options.path +=(options.path.charAt(url.length-1) === '?'?"?":"");
		          options.path = options.path.concat(Util.encodeQueryFromArgs(args.parameters));
		          debug("options.path after request parameters = ", options.path);
		        }

		        // override client config, by the moment just for request response config
		        this.overrideClientConfig(options,args);
			}
			

			debug("options post connect",options);
			debug("FINAL SELF object  ====>", self);

			if (self.useProxy && self.useProxyTunnel){
				connectManager.proxy(options,callback);
			}else{
				// normal connection and direct proxy connections (no tunneling)
				connectManager.normal(options,callback);
			}
		},
		mergeMimeTypes:function(mimetypes){
			// merge mime-types passed as options to client
			if (mimetypes && typeof mimetypes === "object"){
				if (mimetypes.json && mimetypes.json instanceof Array && mimetypes.json.length > 0){
					connectManager.jsonctype = mimetypes.json;
				}else if (mimetypes.xml && mimetypes.xml instanceof Array && mimetypes.xml.length > 0){
					connectManager.xmlctype = mimetypes.xml;
				}
			}
		},
		createHttpMethod:function(methodName){
			return function(url, args, callback){
				var clientRequest = new ClientRequest();		
				Util.connect(methodName.toUpperCase(), url, args, callback, clientRequest);
				return clientRequest;
			};
		}
	},
	Method = function(url, method){
		var httpMethod = self[method.toLowerCase()];
			
		    return  function(args,callback){
					var completeURL = url;
					//no args
					if (typeof args === 'function'){
						callback = args;
						args = {};
					}else if (typeof args === 'object'){
					// we have args, go and check if we have parameters
						if (args.parameters && Object.keys(args.parameters).length > 0){
							// validate URL consistency, and fix it
							url +=(url.charAt(url.length-1) === '?'?"?":"");
							completeURL = url.concat(Util.encodeQueryFromArgs(args.parameters));
							//delete args parameters we don't need it anymore in registered
							// method invocation
							delete args.parameters;
						}
					}
					return httpMethod(completeURL, args , callback);
				};
	};




	this.get = Util.createHttpMethod("get");

	this.post = Util.createHttpMethod("post");
	
	this.put = Util.createHttpMethod("put");

	this.delete = Util.createHttpMethod("delete");

	this.patch = Util.createHttpMethod("patch");


	this.registerMethod = function(name, url, method){
		// create method in method registry with preconfigured REST invocation
		// method
		this.methods[name] = new Method(url,method);
	};

	this.unregisterMethod = function(name){
		delete this.methods[name];
	};

	this.addCustomHttpMethod=function(methodName){
		self[methodName.toLowerCase()] = Util.createHttpMethod(methodName);
	}

	// handle ConnectManager events
	connectManager.on('error',function(err){
		self.emit('error',err);
	});
	
	// merge mime types with connect manager
	Util.mergeMimeTypes(self.mimetypes);
	debug("ConnectManager", connectManager);

};


	var ConnectManager = function() {
		this.xmlctype  = ["application/xml","application/xml;charset=utf-8","text/xml","text/xml;charset=utf-8"];
		this.jsonctype = ["application/json","application/json;charset=utf-8"],
		this.isXML = function(content){
			var result = false;
			if (!content) return result;

			for (var i=0; i<this.xmlctype.length;i++){
				result = this.xmlctype[i].trim().toLowerCase() === content.trim().toLowerCase();
				if (result) break;
			}
			
			return result;
		},
		this.isJSON = function(content){
			var result = false;
			if (!content) return result;

			for (var i=0; i<this.jsonctype.length;i++){
				result = this.jsonctype[i].trim().toLowerCase() === content.trim().toLowerCase();
				if (result) break;
			}
			
			return result;
		},
		this.isValidData = function(data){
			return data !== undefined && (data.length !== undefined && data.length > 0);
		},
		this.configureRequest = function(req, config, clientRequest){

			if (config.timeout){
				req.setTimeout(config.timeout, function(){
					clientRequest.emit('requestTimeout',req);
				});
			}
				

			if(config.noDelay)
				req.setNoDelay(config.noDelay);

			if(config.keepAlive)
				req.setSocketKeepAlive(config.noDelay,config.keepAliveDelay || 0);
			
		},
		this.configureResponse = function(res,config, clientRequest){
			if (config.timeout){
				res.setTimeout(config.timeout, function(){
					clientRequest.emit('responseTimeout',res);
					res.close();
				});
			}
		},
		this.handleEnd = function(res,buffer,callback){
			
			var self = this,
				content = res.headers["content-type"],
				encoding = res.headers["content-encoding"];
			
			debug("content-type: ", content);
			debug("content-encoding: ",encoding);

			if(encoding !== undefined && encoding.indexOf("gzip") >= 0){
				debug("gunzip");
				zlib.gunzip(Buffer.concat(buffer),function(er,gunzipped){
					self.handleResponse(res,gunzipped,callback);
				});
			}else if(encoding !== undefined && encoding.indexOf("deflate") >= 0){
				debug("inflate");
				zlib.inflate(Buffer.concat(buffer),function(er,inflated){
					self.handleResponse(res,inflated,callback);
				});
			}else {
				debug("not compressed");
				self.handleResponse(res,Buffer.concat(buffer),callback);
			}
		},
		this.handleResponse = function(res,data,callback){
			var content = res.headers["content-type"] && res.headers["content-type"].replace(/ /g, '');

			debug("response content is [",content,"]");
			// XML data need to be parsed as JS object
			if (this.isXML(content)){
				debug("detected XML response");
				parseString(data.toString(), function (err, result) {
					callback(result, res);
			    });
			}else if (this.isJSON(content)){
				debug("detected JSON response");
				var jsonData,
				data = data.toString();
                try {
                    jsonData = this.isValidData(data)?JSON.parse(data):data;
                } catch (err) {
                    // Something went wrong when parsing json. This can happen
                    // for many reasons, including a bad implementation on the
                    // server.
                    jsonData = 'Error parsing response. response: [' +
                                data + '], error: [' + err + ']';

                }
				callback(jsonData, res);
			}else{
				debug("detected unknown response");
				callback(data, res);
			}
		},
		this.prepareData = function(data){
			var result;
			if ((data instanceof Buffer) || (typeof data !== 'object')){
				result = data;
			}else{
				result = JSON.stringify(data);
			}
			return result; 
		},
		this.proxy = function(options, callback){

			debug("proxy options",options.proxy);
			
			// creare a new proxy tunnel, and use to connect to API URL
			var proxyTunnel = http.request(options.proxy),
				self = this;
		
			
			proxyTunnel.on('connect',function(res, socket, head){
				debug("proxy connected",socket);

				// set tunnel socket in request options, that's the tunnel itself
				options.socket = socket;

				var buffer=[],
					protocol = (options.protocol =="http")?http:https,
					clientRequest = options.clientRequest,
					requestConfig = options.requestConfig,
					responseConfig = options.responseConfig;
				
				//remove "protocol" and "clientRequest" option from options, cos is not allowed by http/hppts node objects
				delete options.protocol;
				delete options.clientRequest;
				delete options.requestConfig;
				delete options.responseConfig;

				// add request options to request returned to calling method
				clientRequest.options = options;

				var request = protocol.request(options, function(res){
						//configure response
						self.configureResponse(res,responseConfig, clientRequest);

						// concurrent data chunk handler
						res.on('data',function(chunk){
							buffer.push(new Buffer(chunk));
						});

						res.on('end',function(){
							self.handleEnd(res,buffer,callback);
						});


						// handler response errors
						res.on('error',function(err){
							if (clientRequest !== undefined && typeof clientRequest === 'object'){
								// add request as property of error
								err.request = clientRequest;
								err.response =  res;
								// request error handler
								clientRequest.emit('error',err);
							}else{
								// general error handler
								self.emit('error',err);
							}
						});
				});
				


				// configure request and add it to clientRequest
				// and add it to request returned
				self.configureRequest(request,requestConfig, clientRequest);
				clientRequest.setHttpRequest(request);


				// write POST/PUT data to request body;
				if(options.data) request.write(self.prepareData(options.data));


				// handle request errors and handle them by request or general error handler
				request.on('error',function(err){
					if (clientRequest !== undefined && typeof clientRequest === 'object'){
						// add request as property of error
						err.request = clientRequest;
						
						// request error handler
						clientRequest.emit('error',err);
					}else{
						// general error handler
						self.emit('error',err);
					}
				});


				request.end();
			});

			// proxy tunnel error are only handled by general error handler
			proxyTunnel.on('error',function(e){
				self.emit('error',e);
			});

			proxyTunnel.end();
			
		},
		this.normal = function(options, callback){

				var buffer = [],
				protocol = (options.protocol === "http")?http:https,
				clientRequest = options.clientRequest,
				requestConfig = options.requestConfig,
				responseConfig = options.responseConfig,
				self = this;
				
				//remove "protocol" and "clientRequest" option from options, cos is not allowed by http/hppts node objects
				delete options.protocol;
				delete options.clientRequest;
				delete options.requestConfig;
				delete options.responseConfig;
				debug("options pre connect", options);

				// add request options to request returned to calling method
				clientRequest.options = options;

				var request = protocol.request(options, function(res){
						//configure response
						self.configureResponse(res,responseConfig, clientRequest);

						// concurrent data chunk handler
						res.on('data',function(chunk){
							buffer.push(new Buffer(chunk));
						});

						res.on('end',function(){

							self.handleEnd(res,buffer,callback);

						});

						// handler response errors
						res.on('error',function(err){
							if (clientRequest !== undefined && typeof clientRequest === 'object'){
								// add request as property of error
								err.request = clientRequest;
								err.response = res;
								// request error handler
								clientRequest.emit('error',err);
							}else{
								// general error handler
								self.emit('error',err);
							}
						});
				});
			 
				// configure request and add it to clientRequest
				// and add it to request returned
				self.configureRequest(request,requestConfig, clientRequest);
				debug("clientRequest",clientRequest);

				clientRequest.setHttpRequest(request);

				// handle request errors and handle them by request or general error handler
				request.on('error',function(err){
					debug('request error', clientRequest);
					if (clientRequest !== undefined && typeof clientRequest === 'object'){
						// add request as property of error
						err.request = clientRequest;
						// request error handler
						clientRequest.emit('error',err);
					}else{
						// general error handler
						self.emit('error',err);
					}
				});


			 debug("options data", options.data);
			// write POST/PUT data to request body;
			if(options.data) request.write(this.prepareData(options.data));

			request.end();
			 
		}
	};


	// event handlers for client and ConnectManager
	util.inherits(exports.Client, events.EventEmitter);
	util.inherits(ConnectManager,events.EventEmitter);


	var debug = function(){
		if (!process.env.DEBUG) return;

		var now = new Date(),
			header =now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() +  " [NRC CLIENT]" + arguments.callee.caller.name + " -> ",
			args = Array.prototype.slice.call(arguments);
		args.splice(0,0,header);
		node_debug.apply(console,args);


	};
