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

exports.Client = function (options){
    var self = this,
     // parser response manager
     parserManager = require("./nrc-parser-manager")(),
     serializerManager = require("./nrc-serializer-manager")(),
    // connection manager
    connectManager = new ConnectManager(this, parserManager),
    // io facade to parsers and serailiazers
    ioFacade = function(parserManager, serializerManager){
        // error execution context
        var  errorContext = function(logic){
            return function(){
                try{
                    return logic.apply(this, arguments);
                }catch(err){
                    self.emit('error',err);
                }
              };
        },
        result={"parsers":{}, "serializers":{}};

    // parsers facade
    result.parsers.add = errorContext(parserManager.add);
    result.parsers.remove = errorContext(parserManager.remove);
    result.parsers.find = errorContext(parserManager.find);
    result.parsers.getAll = errorContext(parserManager.getAll);
    result.parsers.getDefault = errorContext(parserManager.getDefault);
    result.parsers.clean = errorContext(parserManager.clean);
    
    // serializers facade
    result.serializers.add = errorContext(serializerManager.add);
    result.serializers.remove = errorContext(serializerManager.remove);
    result.serializers.find = errorContext(serializerManager.find);
    result.serializers.getAll = errorContext(serializerManager.getAll);
    result.serializers.getDefault = errorContext(serializerManager.getDefault);
    result.serializers.clean = errorContext(serializerManager.clean);

    return result;
    
    }(parserManager,serializerManager),
    // declare util constants
    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 || {};

    // namespaces for methods, parsers y serializers
    this.methods={};
    this.parsers={};
    this.serializers={};

    // 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 (var proxyOption in result.proxy){
                    result[proxyOption] = result.proxy[proxyOption];
                }

                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;

        },
        serializeEncodeQueryFromArgs:function(args){

            function serialize(obj, parent) {
              var tokens = [], propertyName;
              // iterate over all properties
              for(propertyName in obj) {
                  // if object has property (it's not an array iteration)
                  if (obj.hasOwnProperty(propertyName)) {
                  // if property has parent, add nested reference
                  var parsedProperty = parent ? parent + "[" + propertyName + "]" : propertyName, propertyValue = obj[propertyName];

                  // if property has value and is object (we must iterate
					// again, not final leaf)
                  // iterate over object property passing current parsed
					// property as parent
                  // else add encoded parsed property and value to result
					// array
                  tokens.push((propertyValue !== null && typeof propertyValue === "object") ?
                    serialize(propertyValue, parsedProperty) :
                    encodeURIComponent(parsedProperty) + "=" + encodeURIComponent(propertyValue));
                    }
                }
                    return tokens.join("&");
            }

          debug("args is", args);
                // check args consistency
                if (args && typeof args !== 'object' )
                    self.emit('error','cannot serialize parameters: invalid type ' + (typeof  args) + ' should be an object type');

                return serialize(args);
        },
        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){
            //wrapper for emit function on client
            var clientEmitterWrapper = function (client){
                    var client = client;
                    return function(type, event){client.emit(type, event);};
                };

            // check args type if we use it
            if (callback && args && typeof args !== 'object')self.emit('error','args should be and object');

            // 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];
                        }
                    }
                    
                }
                
 
                // 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 adding query
					// parameter separation char

                  // check if URL already has '?' path parameter separator
					// char in any position that is not final
                  // if true throw error
                  var pathLength = options.path.length,
                  pathParameterSepCharPos = options.path.indexOf("?");

                  if (pathParameterSepCharPos >= 0 && pathParameterSepCharPos!== pathLength -1 )
                   self.emit('error','parameters argument cannot be used if parameters are already defined in URL ' + options.path);

               options.path +=(options.path.charAt(pathLength-1) === '?'?"":"?");
                  // check if we have serializable parameter container, that
					// must be serialized and encoded
                  // directly, as javascript object
                  options.path = options.path.concat(Util.serializeEncodeQueryFromArgs(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);

               // 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)){                    
                    serializerManager.get(options).serialize(args.data, clientEmitterWrapper(self), function(serializedData){
                        options.data = serializedData;
                        options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = Buffer.byteLength(options.data, 'utf8');
                    });                    
                }else{
                    options.headers[CONSTANTS.HEADER_CONTENT_LENGTH] = 0;
                }


            }
            

            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){
        	// this function is left for backward compatibility, but will be
        	// deleted in future releases
        	var parser = null;
            // merge mime-types passed as options to parsers
            if (mimetypes && typeof mimetypes === "object"){
	            try{
	            	if (mimetypes.json && mimetypes.json instanceof Array && mimetypes.json.length > 0){                	
                		parser = parserManager.find("JSON");
                		parser.contentTypes = mimetypes.json;                    
	                }else if (mimetypes.xml && mimetypes.xml instanceof Array && mimetypes.xml.length > 0){
	                	parser = parserManager.find("XML");
	            		parser.contentTypes = mimetypes.xml;                    
	                }
	        	}catch(err){
	        		self.emit('error', 'cannot assign custom content types to parser, cause: ' + err);
	        	}
            }
        },
        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 = {};
                    }

                    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);
    };

    this.parsers = ioFacade.parsers;

    this.serializers = ioFacade.serializers;
   
    // merge mime types with connect manager
    Util.mergeMimeTypes(self.mimetypes);
    debug("ConnectManager", connectManager);

};


var ConnectManager = function(client, parserManager) {
   
    var client = client,
    clientEmitterWrapper = function (client){
                    var client = client;
                    return function(type, event){client.emit(type, event);};
                };
    
  

    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.configureOptions = function(options){
        var followRedirectsProps =["followRedirects", "maxRedirects"];
        function configureProps(propsArray, optionsElement){
            for (var index in propsArray){
                if (optionsElement.hasOwnProperty(propsArray[index]))
                    options[propsArray[index]] = optionsElement[propsArray[index]];        
            }    
        }
        
        //add follows-redirects config
        configureProps(followRedirectsProps, options.requestConfig);
        

        // 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);
    };

    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){
        // find valid parser to be used with response content type, first one
		// found
        parserManager.get(res).parse(data, clientEmitterWrapper(client), function(parsedData){
            callback(parsedData,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;
                
                self.configureOptions(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(Buffer.from(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
                                client.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;
                // find valid serializer to be used to serialize request data,
				// first one found
                // is the one to be used.if none found for match condition,
				// default serializer is used

               if(options.data)request.write(options.data);

                request.end();
                
               
               // 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
                       client.emit('error',err);
                   }
               });
            });

            // proxy tunnel error are only handled by general error handler
            proxyTunnel.on('error',function(e){
                client.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;
            
                self.configureOptions(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(Buffer.from(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
                                client.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);

                debug("options data", options.data);
                // write POST/PUT data to request body;
                // find valid serializer to be used to serialize request data,
				// first one found
                // is the one to be used.if none found for match condition,
				// default serializer is used
                if(options.data)request.write(options.data);
                request.end(); // end request when data is written

                // 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
                        client.emit('error',err);
                    }
                });            
        };
    };


    // event handlers for client and ConnectManager
    util.inherits(exports.Client, 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);


    };
