var http = require('http');
var https = require('https');
var Request = require('./Request.js').Request;
var Response = require('./Response.js').Response;
var Event = require('../event/Event.js').Event;
var ErrorEvent = require('../event/ErrorEvent.js').ErrorEvent;
var EventDispatcher = require('../event/EventDispatcher.js').EventDispatcher;
var util = require('util');

var Loader = function (request) {
    EventDispatcher.call(this);

    this._buffer = null;
    this._request = null;
    this._response = null;
    this._requestStart = null;
    this._requestLength = null;
    this._responseLength = null;
    this._connectAt = null;
    this._timeout = 8000;
    this._connTimeout = null;
    this._timer = null;
    this._connTimer = null;
    this._followRedirects = true;
    this._maxRedirectsCount = 10;
    this._currentRedirectsCount = 0;
    this._dataBuffering = true;
    this._agent = false;
    this._forwardedHost = null;
    this._pipelined = false;
    this._upgrade = null;

    this.setRequest(request);
};

Loader.prototype = Object.create(EventDispatcher.prototype);

Loader.prototype.setDataBuffering = function (buffering) {
    this._dataBuffering = !!buffering;
    return this;
};

Loader.prototype.setForwardedHost = function (host) {
    this._forwardedHost = host;
    return this;
};

Loader.prototype.getForwardedHost = function () {
    return this._forwardedHost;
};

/**
 * callback responsible for gathering response chunks
 * @private
 * @param {Server.HttpResponse} serverResponse
 */
Loader.prototype._handleServerResponse = function(serverResponse){
    var dataLength = 0,
        buffers = [],
        self = this;

    if (this.isPipelined()) {
        this.dispatchEvent(new Event(Loader.Event.HEADERS, {
            'statusCode': serverResponse.statusCode,
            'headers': serverResponse.headers,
            'httpVersion': serverResponse.httpVersion,
            'body': null
        }));
    }

    serverResponse.addListener('data', function (buffer) {
        dataLength += buffer.length;

        if (self.isPipelined()) {
            self.dispatchEvent(new Event(Loader.Event.PROGRESS, buffer));
        } else {
            if (self._dataBuffering) {
                buffers.push(buffer);
            }
        }
    });

    if (!this.isPipelined()) {
        serverResponse.addListener('end', function () {
            self._buffer = new Buffer(dataLength);

            var bufferPosition = 0;

            for (var i = 0, l = buffers.length; i < l; i++) {
                buffers[i].copy(self._buffer, bufferPosition);
                bufferPosition += buffers[i].length;
                delete buffers[i];
            }

            buffers = [];

            self._responseLength = dataLength;
            self._onEnd(serverResponse);
        });
    } else {
        serverResponse.addListener('end', function () {
            self._responseLength = dataLength;
            self._log(serverResponse);
        });
    }
};

/**
 * sets timer who is responsible for timeout detection
 * @private
 * @param {Server.HttpRequest} request
 */
Loader.prototype._setTimer = function (timeout) {
    var self = this;

    var onTimeout = function () {
        self.abort();
        self._log({ statusCode: Loader.ExceptionCode.TIMEOUT });
        self.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, Loader.ExceptionCode.TIMEOUT, 'Http response timeout'));
    };

    if (timeout < 0) {
        return onTimeout();
    }

    this._timer = setTimeout(function () {
        onTimeout();
    }, timeout);
};

Loader.prototype._setConnTimer = function (timeout) {
    // socket might not be assigned yet
    if (!this._clientRequest.socket) {
        this._clientRequest.once('socket', this._setConnTimer.bind(this, timeout));
        return;
    }

    var that = this;

    this._connTimer = setTimeout(function () {
        that.abort();
        that._log({ statusCode: Loader.ExceptionCode.CONNECT_TIMEOUT });
        that.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, Loader.ExceptionCode.CONNECT_TIMEOUT, 'Connect timeout'));
    }, timeout);
};

/**
 * clears timer handle that is responsible for timeout detection
 * @private
 */
Loader.prototype._clearTimer = function () {
    clearTimeout(this._connTimer);
    clearTimeout(this._timer);
};

Loader.prototype._clearConnTimer = function () {
    clearTimeout(this._connTimer);
};

Loader.prototype._clearChunks = function () {
    this._buffer = [];
};

Loader.prototype._log = function (res, type, suffix) {
    if (!this._forwardedHost) {
        return;
    }

    var req = this.getRequest();
    var code = res ? res.statusCode : 0;
    var len = this._responseLength || 0;
    var upstream = req.getHeader('host') || '-';
    var gateway = req.getConnectionHost() || '-';
    var tt = Date.now() - this._requestStart;
    var ct = this._connectAt ? (this._connectAt - this._requestStart) : Date.now() - this._requestStart;
    var rel = this._requestLength || 0;

    if (req.getProto() === 'https:') {
        type = type || 'https';
    } else {
        type = type || 'http';
    }

    if (this._upgrade) {
        type += '-' + this._upgrade;
    }

    var str = 'Loader/Monitoring: vhost=%s upstream=%s code=%s tt=%s len=%s method=%s gateway=%s type=%s reqlen=%s ct=%s';

    if (suffix) {
        str += ' ' + suffix;
    }

    console.info(str, this._forwardedHost, upstream, code, tt, len, req.getMethod(), gateway, type, rel, ct);
};

/**
 * called on request end, when whole data is retreived from server
 * @private
 */
Loader.prototype._onEnd = function (response) {
    var requestMethod = this._request.getMethod();

    this._log(response);

    if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.hasOwnProperty('location') && this._followRedirects && requestMethod != Request.PUT && requestMethod != Request.POST) {
        if (this._currentRedirectsCount >= this._maxRedirectsCount) {
            this.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, Loader.ExceptionCode.MAX_REDIRECTS, 'maxRedirectsCount reached, stopping'));
            return;
        }

        this._currentRedirectsCount++;
        this._request.setUrl(response.headers.location);
        //XXX: ale to jest slabe :(
        this._request.setHeader('host', this._request.getConnectionHost(), true);
        this._clearChunks();
        return this.load();
    }


    var data = this._parseResponse(response);

    if (data === false) {
        //tutaj zakladamy ze _parseResponse obsluzyl blad
        return;
    }

    this.dispatchEvent(new Event(Loader.Event.LOADED, data));
};

Loader.prototype._parseResponse = function (rawResponse) {
    if (!this._response) {
        this._response = new Response({
            'statusCode': rawResponse.statusCode,
            'headers': rawResponse.headers,
            'httpVersion': rawResponse.httpVersion,
            'body': this._buffer
        });
    } else {
        this._response.setStatusCode(rawResponse.statusCode);
        this._response.setHeaders(rawResponse.headers);
        this._response._setBody(this._buffer);
    }

    return this._response;
};

Loader.prototype._createRequest = function (request) {
    var ua = request.getHeader('user-agent');

    if (!ua) {
        request.setHeader('user-agent', 'nodejs', true);
    }

    var host = request.getConnectionHost() || request.getHost();
    var port = request.getPort();
    var path = request.getPath();
    var method = request.getMethod();
    var headers = request.getHeaders();

    if (!host || !port || !path || !method || !headers) {
        this._log({ statusCode: Loader.ExceptionCode.INCOMPLETE_REQUEST });
        return this.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, null, Loader.ExceptionCode.INCOMPLETE_REQUEST, Loader.Exception.INCOMPLETE_REQUEST));
    }

    //FIXME to pasuje rozwiazac w klasie Request gdy klonuje inny request
    host = host.replace(':'+request.getPort(), '');

    var retValue = {
        host: host,
        port: port,
        path: path + request.getQueryString(),
        method: method,
        headers: headers
    };

    if (!this._agent) {
        retValue.agent = false;
    }

    return retValue;
};

Loader.prototype._pipe = function (dst, serverResponse) {
    var that = this;
    var src = this._request.getSocket();
    var reqLength = 0;
    var resLength = 0;

    //remote
    dst.on('data', function (data) {
        resLength += data.length;
        that.dispatchEvent(new Event(Loader.Event.PROGRESS, data));
    });

    dst.on('end', function () {
        src.end();
    });

    //local
    src.on('data', function (data) {
        reqLength += data.length;
        dst.write(data);
    });

    src.on('end', function () {
        that._requestLength = reqLength;
        that._responseLength = resLength;
        that._log(serverResponse);
        that.dispatchEvent(new Event(Loader.Event.LOADED, null));
        dst.end();
    });

    src.resume();
};

/**
 * sets timeout in miliseconds
 * @param {Number} ms
 */
Loader.prototype.setTimeout = function (ms) {
    this._timeout = ms;
    return this;
};

/**
 * sets connect timeout in milliseconds
 * @param {Number} ms
 */
Loader.prototype.setConnectTimeout = function (ms) {
    this._connTimeout = ms;
    return this;
};

/**
 * gets timeout in miliseconds
 * @returns {Number}
 */
Loader.prototype.getTimeout = function () {
    return this._timeout;
};

Loader.prototype.getConnectTimeout = function () {
    return this._connTimeout;
};

Loader.prototype.setFollowRedirects = function (follow) {
    this._followRedirects = !!follow;
    return this;
};

Loader.prototype.getFollowRedirects = function () {
    return this._followRedirects;
};

Loader.prototype.setMaxRedirectsCount = function (count) {
    this._maxRedirectsCount = count;
    return this;
};

Loader.prototype.getMaxRedirectsCount = function () {
    return this._maxRedirectsCount;
};

Loader.prototype._cloneRequest = function (request) {
    return new Request(request);
};

/**
 * sets request object to process
 * @param {OpalRequest} request
 */
Loader.prototype.setRequest = function (request) {
    if(request && !(request instanceof Request)){
        throw Loader.Exception.WRONG_REQUEST_INSTANCE;
    } else if(request) {
        this._request = this._cloneRequest(request);
        this._currentRedirectCount = 0;
        this._clearChunks();
    }

    return this;
};

Loader.prototype.setResponse = function (response) {
    if (response) {
        this._response = response;
    }

    return this;
};

/**
 * returns request (a copy of it) which is going to be processed
 */
Loader.prototype.getRequest = function () {
    return this._request;
};

/**
 * Aborts a request, clears timeout, dispatches ABORTED event
 */
Loader.prototype.abort = function () {
    if (this._clientRequest) {
        this._clientRequest.removeAllListeners('response');
        this._clientRequest.abort();
    }

    this._clearTimer();
    this.dispatchEvent(new Event(Loader.Event.ABORTED, 'Aborted'));
};

/**
 * Sets agent value
 * @param {Boolean} value
 */
Loader.prototype.setAgent = function (value) {
    this._agent = !!value;
};

Loader.prototype.setPipelined = function (bool) {
    this._pipelined = !!bool;
    return this;
};

Loader.prototype.isPipelined = function () {
    return this._pipelined;
};

/**
 * makes request to the backend system based on the request object
 * @param {Request} request
 */
Loader.prototype.load = function (request, response) {
    var that = this;

    this.setRequest(request);
    this.setResponse(response);

    if (this._request === null) {
        throw Loader.Exception.REQUEST_NOT_SET;
    }

    this._requestStart = Date.now();
    this._connectAt = null;

    var options = this._createRequest(this._request);

    var httpClient = http;
    if(this._request.getProto() == 'https:') {
        httpClient = https;
    }

    if (options) {
        this._clientRequest = httpClient.request(options, function(serverResponse){
            that._clearTimer();
            that._handleServerResponse(serverResponse);
        });

        var connTimeout = this.getConnectTimeout();

        if (connTimeout) {
            this._setConnTimer(connTimeout);
        } else {
            this._setTimer(this.getTimeout());
        }

        this._clientRequest.socket.once('connect', function () {
            that._clearConnTimer();
            that._connectAt = Date.now();

            // if connect timeout was set we need to create read timer now
            if (connTimeout) {
                var resTimeout = that.getTimeout() - (that._connectAt - that._requestStart); // total timeout - connect time
                that._setTimer(resTimeout);
            }
        });

        this._clientRequest.agent.addListener('upgrade', function (serverResponse, socket, head) {
            that._upgrade = serverResponse.headers['upgrade'];
            that._clearTimer();
            that._log(serverResponse);

            that.dispatchEvent(new Event(Loader.Event.HEADERS, {
                'statusCode': serverResponse.statusCode,
                'headers': serverResponse.headers,
                'httpVersion': serverResponse.httpVersion,
                'body': null,
                'head': head
            }));

            that._pipe(socket, serverResponse);
        });

        this._clientRequest.addListener('error', function (e) {
            options.agent = null;
            console.error('LOADER ERROR NODE REQUEST: ' + util.inspect(e, true, 20) + ' Options: ' + util.inspect(options, true, 20));
            that._clearTimer();
            that._log({ statusCode: Loader.ExceptionCode.GENERIC });
            that.dispatchEvent(new ErrorEvent(Loader.Event.ERROR, e, Loader.ExceptionCode.GENERIC, 'Http request error: ' + e.message));
        });

        // jezeli sa dane np POST to tez je wpisuje
        if (!this.isPipelined() && this._request.getBody().length() > 0) {
            var buf = this._request.getBody().toBuffer();

            this._requestLength = buf.length;
            this._clientRequest.write(buf);
        }

        this._clientRequest.end();
    }
};

Loader.Event = {};
Loader.Event.LOADED   = 'Loader_LOADED';
Loader.Event.ABORTED  = 'Loader_ABORTED';
Loader.Event.ERROR    = 'Loader_ERROR';
Loader.Event.HEADERS  = 'Loader_HEADERS';
Loader.Event.PROGRESS = 'Loader_PROGRESS';
Loader.Event.UPGRADE  = 'Loader_UPGRADE';

Loader.Exception = {};
Loader.Exception.INCOMPLETE_REQUEST = 'Given request misses some key values';
Loader.Exception.REQUEST_NOT_SET = 'Request has not been set';
Loader.Exception.WRONG_REQUEST_INSTANCE = 'Given request is not a instance of Request';
Loader.Exception.FOLLOW_REDIRECTS_TYPE_ERROR = 'setFollowRedirects requires an argument to be a boolean';
Loader.Exception.AGENT_VALUE = 'Agent param value must be a type of Boolean';

Loader.ExceptionCode = {};
Loader.ExceptionCode.GENERIC = -1;
Loader.ExceptionCode.TIMEOUT = -3;
Loader.ExceptionCode.MAX_REDIRECTS = -4;
Loader.ExceptionCode.INCOMPLETE_REQUEST = -5;
Loader.ExceptionCode.CONNECT_TIMEOUT = -6;

exports.Loader = Loader;
