/**
 * @overview Request
 * @copyright Dreamlab Onet.pl sp. z. o o 2012
 */

var http = require('http');
var urlParser = require('uriparser');

var Types = require('../common/Types.js').Types;
var Message = require('./Message.js').Message;
var BinaryData = require('../data/BinaryData.js').BinaryData;
var FormData = require('../data/FormData.js').FormData;


/**
 * Http Request
 * @class Request
 * @classdesc Object that represents http request
 * @extends Message
 * @throw Request.Exception.REQUEST_CANNOT_BE_A_BUFFER
 * @param {Object|String|Request|Buffer|http.IncomingMessage} params
 * @return {Request}
 */
var Request = function(params){
    Message.call(this, params);
    
    this._url = null;
    this._method = 'GET';
    this._host = null;
    this._port = 80;
    this._path = '/';
    this._proto = 'http:';
    this._authUser = null;
    this._authPassword = null;
    this._query = {};
    this._clientAddress = null;
    this._clientPort = null;
    this._timeStart = null;

    if (Buffer.isBuffer(params)) {
        throw Request.Exception.REQUEST_CANNOT_BE_A_BUFFER;
    } else if (Types.isString(params)) {
        this.setUrl(params);
    } else if (Types.isInstanceOf(params, Request)) {
        this.setUrl(params.getUrl());
        this.setMethod(params.getMethod());
        this.setHeaders(params.getHeaders());
        this.setBody(params.getBody());
        this.setHttpVersion(params.getHttpVersion());
        //connection host
        this.setConnectionHost(params.getConnectionHost());
    } else if (Types.isInstanceOf(params, http.IncomingMessage)) {
        // 1831 - 1886
        // 1500 - 1618
        this.setMethod(params.method);
        // 1483 - 1572
        this.setHeaders(params.headers);
        // 1416 - 1552
        var httpVer = params.httpVersion;
        // 1381 - 1483
        this.setHttpVersion(httpVer);
        // 1381 - 1483 
        // 1362 - 1488   (bez return this)  1388 - 1492
        this.setBody(params.data);
        // 1340 - 1453
        this.setUrl(params.url);
        this._clientAddress = params.client.remoteAddress;
        this._clientPort = params.client.remotePort;
        this._timeStart = (params.__timeStart || Date.now());
    } else {
        this.init(params);
    }
};

Request.prototype = Object.create(Message.prototype);

Request.prototype._cookieRegExpCache = {};
Request.prototype._cookieRegExp = "=([^;]+)(;\\s?|$)"; 

/**
 * Checks if given string is already encoded
 * @method
 * @private
 * @param {String} str
 * @return {Boolean}
 */
Request.prototype._isEncoded = function (str) {
    return (str.indexOf("%") != -1 || str.indexOf("+") != -1);
};

/**
 * Set cookie
 * @method
 * @param {String} cookie
 * @return {Request}
 */
Request.prototype.setCookie = function (cookie) {
    var cookies = this.headers.getHeader("cookie");

    if (!cookies) {
        cookies = '';
    }

    if (cookies.length) {
        cookies += ' ' + cookie;
    } else {
        cookies = cookie;
    }

    this.headers.setHeader("cookie", cookies, true);

    return this;
};

/**
 * Get cookie
 * @method
 * @param {String} cookieName
 * @return {String|null}
 */
Request.prototype.getCookie = function (cookieName) {
    var cookies = this.headers.getHeader("cookie"), regexp;

    if (!cookies) {
        return null;
    }

    if (this._cookieRegExpCache[cookieName]) {
        regexp = this._cookieRegExpCache[cookieName];
    } else {
        regexp = this._cookieRegExpCache[cookieName] = new RegExp(cookieName + this._cookieRegExp);
    }

    var result = cookies.match(regexp);

    if (result) {
        return result[1];
    }

    return null;
};

/**
 * Remove cookie
 * @method
 * @param {String} cookieName
 * @return {null|Boolean}
 */
Request.prototype.removeCookie = function (cookieName) {
    var cookies = this.headers.getHeader("cookie"), regexp;

    if (!cookies) {
        return null;
    }

    if (this._cookieRegExpCache[cookieName]) {
        regexp = this._cookieRegExpCache[cookieName];
    } else {
        regexp = this._cookieRegExpCache[cookieName] = new RegExp(cookieName + this._cookieRegExp);
    }

    cookies = cookies.replace(regexp, '');

    this.headers.setHeader("cookie", cookies, true);

    return true;
};
    
/**
 * Sets url of a service endpoint
 * @method
 * @param {String} url
 * @returns {UrlRequest}
 */
Request.prototype.setUrl = function (url) {
    this._url = url;

    var matches = urlParser.parse(url);
    
    if (matches.hasOwnProperty('protocol')) {
        this.setProto(matches.protocol);
    }

    if (matches.hasOwnProperty('auth')) {
        this.setAuth(matches.auth);
    }
    
    if (matches.hasOwnProperty('host')) {
        this.setConnectionHost(matches.host);
    } else {
        this.setConnectionHost(this.getHost());
    }

    if (matches.hasOwnProperty('port')) {
        this.setPort(matches.port);
    }

    if (matches.hasOwnProperty('path')) {
        this.setPath(matches.path);
    }

    if (matches.hasOwnProperty('query')) {
        this.setQuery(matches.query);
    }

    return this;
};

/**
 * Returns url of a service endpoint
 * @method
 * @returns {String}
 */
Request.prototype.getUrl = function () {
    if (this._url) {
        return this._url;
    }

    var url = '';

    var proto = this.getProto();
    if (proto) {
        url += proto + '//';
    }

    var auth = this.getAuth();
    if (auth) {
        url += auth + '@';
    }

    var host = this.getConnectionHost(), 
        port = this.getPort();
    
    if (this.getHost()) {
        host = this.getHost();
        if (host.lastIndexOf(':') > 0) {
            port = null;
        }
    }    
    if (port) {
        url += host + ':' + port;
    } else {
        url += host;
    }

    var path = this.getPath();
    if (path) {
        url += path;
    } else {
        url += '/';
    }
    
    url += this.getQueryString();

    this._url = url;
    return url;
};

/**
 * Returns query string component of an url
 * @method
 * @returns {String}
 */
Request.prototype.getQueryString = function () {
    var url = '';
    var query = this.getQuery()
    if (query) {
        url += '?';

        var tmp = '';
        // to nie zadziala dla case   test ącki
        for (var key in query) {
            if (query.hasOwnProperty(key)) {
                if (Array.isArray(query[key])) {
                    for (var i = 0, max = query[key].length; i < max; i++) {
                        var value = query[key][i];
                        tmp += this._isEncoded(key) ? key : encodeURIComponent(key);
                        tmp += "%5B%5D";
                        tmp += "=" + (this._isEncoded(value) ? value : encodeURIComponent(value));
                        tmp += '&';
                    }
                } else {
                    tmp += this._isEncoded(key) ? key : encodeURIComponent(key);

                    if (query[key]) {
                        tmp += "=" + (this._isEncoded(query[key]) ? query[key] : encodeURIComponent(query[key]));
                    } else {
                        tmp += "=";
                    }

                    tmp += '&';
                }
            }
        }

        url += tmp.substring(0, tmp.length - 1);
    }

    if (url == '?') {
        url = '';
    }

    return url;
};

/**
 * Sets method of communicating with a service
 * @method
 * @param {String} method
 * @returns {Request}
 */
Request.prototype.setMethod = function(method){
    this._method = method;
    return this;
};

/**
 * Returns method of communicating with a service
 * @method
 * @returns {String}
 */
Request.prototype.getMethod = function () {
    return this._method;
};

/**
 * Sets connection host
 * @method
 * @param {String} host
 * @returns {Request}
 */
Request.prototype.setConnectionHost = function (host) {
    this._url = null;
    this._host = host;
    return this;
};

/**
 * Returns connection host
 * @method
 * @returns {String}
 */
Request.prototype.getConnectionHost = function () {
    return this._host;
};


/**
 * Sets Host http header
 * @method
 * @param {String} host
 * @returns {Request}
 */
Request.prototype.setHost = function (host) {
    this._url = null;
    this.headers.setHeader('host', host, true);
    return this;
};

/**
 * Returns Host http header
 * @method
 * @returns {String|null}
 */
Request.prototype.getHost = function () {
    return this.headers.getHeader('host');
};

/**
 * Sets connection port
 * @method
 * @param {Number} port
 * @returns {Request}
 */
Request.prototype.setPort = function (port) {
    this._url = null;
    this._port = port;
    return this;
};

/**
 * Returns connection port
 * @method
 * @returns {Number|null}
 */
Request.prototype.getPort = function () {
    return this._port;
};

/**
 * Sets url path
 * @method
 * @param {String} path
 * @returns {Request}
 */
Request.prototype.setPath = function (path) {
    this._url = null;
    this._path = path;
    return this;
};

/**
 * Returns url path
 * @method
 * @returns {String|null}
 */
Request.prototype.getPath = function () {
    return this._path;
};

/**
 * Sets protocol
 * @method
 * @param {String} proto
 * @returns {Request}
 */
Request.prototype.setProto = function (proto) {
    this._url = null;
    this._proto = proto;

    return this;
};

/**
 * Returns protocol
 * @method
 * @returns {String|null}
 */
Request.prototype.getProto = function () {
    return this._proto;
};

/**
 * Sets http simple auth digest in format: user:pass
 * @method
 * @param {String} auth
 * @returns {Request}
 */
Request.prototype.setAuth = function (auth) {
    this._url = null;

    var data;

    if (Types.isString(auth)) {
        var tmp = auth.split(':');
        data = {user: tmp[0], password: tmp[1]}
    } else {
        data = auth;
    }

    this.setAuthUser(data.user);
    this.setAuthPassword(data.password);

    return this;
};

/**
 * Returns http simple auth digest in format: user:pass
 * @method
 * @returns {String|null}
 */
Request.prototype.getAuth = function () {
    var user = this.getAuthUser(), password = this.getAuthPassword();
    if (user && password) {
        return user + ':' + password;
    }
    return null;
};

/**
 * Sets username for http simple auth digest
 * @method
 * @param {String} user
 * @returns {Request}
 */
Request.prototype.setAuthUser = function (user) {
    this._url = null;
    this._authUser = user;
    return this;
};

/**
 * Returns username for http simple auth digest
 * @method
 * @returns {String|null}
 */
Request.prototype.getAuthUser = function () {
    return this._authUser;
};

/**
 * Sets password for http simple auth digest
 * @method
 * @param {String} password
 * @returns {Request}
 */
Request.prototype.setAuthPassword = function (password) {
    this._url = null;
    this._authPassword = password;
    return this;
};

/**
 * Returns password for http simple auth digest
 * @method
 * @returns {String|null}
 */
Request.prototype.getAuthPassword = function () {
    return this._authPassword;
};

/**
 * Sets url query string
 * @method
 * @param {String} query
 * @returns {Request}
 */
Request.prototype.setQuery = function (query) {
    this._url = null;
    this._query = query;
    return this;
};

/**
 * Returns url query string
 * @method
 * @returns {String|null}
 */
Request.prototype.getQuery = function () {
    return this._query;
};

/**
 * Sets url query component
 * @param {String} name
 * @param {String} value
 * @returns {Request}
 * @method
 */
Request.prototype.setQueryParam = function (name, value) {
    this._url = null;
    if (!this._query) {
        this._query = {};
    }
    this._query[name] = value;
    return this;
};

/**
 * Returns url query component
 * @method
 * @param {String} name
 * @returns {String|null}
 */
Request.prototype.getQueryParam = function (name) {
    if (!this._query) {
        return null;
    }

    return (this._query.hasOwnProperty(name)) ? this._query[name] : null;
};

/**
 * Returns client ip address
 * @method
 * @returns {String|null}
 */
Request.prototype.getClientAddress = function () {
    return (this._clientAddress || null);
};

/**
 * Returns client port number
 * @method
 * @returns {Number|null}
 */
Request.prototype.getClientPort = function () {
    return (this._clientPort || null);
};

/**
 * Returns request processing time
 * @method
 * @returns {Number}
 */
Request.prototype.getProcessingTime = function () {
    return this._timeStart ? (Date.now() - this._timeStart) : 0;
};

/**
 * Sets request body (post data)
 * @method
 * @param {BinaryData|String|null|undefined} body
 * @returns {Request}
 */
Request.prototype.setBody = function (body) {
    if (Types.isInstanceOf(body, FormData)) {
        this.setHeader('content-length', body.length(), true);

        var boundary = body.getBoundary();

        if (boundary) {
            this.setHeader('content-type', 'multipart/form-data; boundary=' + boundary, true);
        }

        Message.prototype.setBody.call(this, body);
    }
    else if (Types.isInstanceOf(body, BinaryData)) {
        Message.prototype.setBody.call(this, body);
    } else {
        Message.prototype.setBody.call(this, new BinaryData(body?body:'', BinaryData.Encoding.TEXT, BinaryData.CharacterEncoding.UTF8));
    }
    return this;
};

/**
 * Determines if caching is disabled
 * @method
 * @returns {Boolean}
 */
Request.prototype.isCachingDisabled = function () {
    if (this.getMethod() == Request.POST) {
        return true;
    }

    return false;
};

/**
 * Determines if request is conditional (can return 304 response)
 * @method
 * @returns {Boolean}
 */
Request.prototype.isConditional = function () {
    return this.getHeader('if-modified-since');
};

/**
 * Determines if request client allow receiving compressed data
 * @method
 * @returns {Boolean}
 */
Request.prototype.isCompressedContentAccepted = function () {
    var ae = this.getHeader('accept-encoding');
    if (ae) {
        return ae.indexOf('gzip') > -1;
    }
    return false;
};

/**
 * Returns JSON representation of a request
 * @method
 * @returns {Object}
 */
Request.prototype.toJson = function () {
    var body = this.getBody();
    return {
        "url": this.getUrl(),
        "method": this._method,
        "headers": this.headers.getHeaders(),
        "body": body ? body.toString() : body,
        "httpVersion": this._httpVersion
    };
};

/**
 * @constant
 * @type {String}
 * @default
 */
Request.OPTIONS = "OPTIONS";
/**
 * @constant
 * @type {String}
 * @default
 */
Request.GET = "GET";
/**
 * @constant
 * @type {String}
 * @default
 */
Request.HEAD = "HEAD";
/**
 * @constant
 * @type {String}
 * @default
 */
Request.POST = "POST";

/**
 * @constant
 * @type {String}
 * @default
 */
Request.PUT = "PUT";

/**
 * @constant
 * @type {String}
 * @default
 */
Request.DELETE = "DELETE";

/**
 * @constant
 * @type {String}
 * @default
 */
Request.TRACE = "TRACE";

/**
 * @constant
 * @type {String}
 * @default
 */
Request.CONNECT = "CONNECT";

/**
 * @static
 * @constant
 * @namespace
 */
Request.Exception = {};

/**
 * @static
 * @constant
 * @type {String}
 * @default
 */
Request.Exception.WRONG_BODY = "Body has to be a instance of BinaryData";

/**
 * @static
 * @constant
 * @type {String}
 * @default
 */
Request.Exception.REQUEST_CANNOT_BE_A_BUFFER = "Request cannot be a Buffer";


exports.Request = Request;
