// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

'use strict';

var url = require('url');
var sprintf = require('util').format;

var assert = require('assert-plus');
var mime = require('mime');
var Negotiator = require('negotiator');
var uuid = require('uuid');

var dtrace = require('./dtrace');

///-- Helpers
/**
 * Creates and sets negotiator on request if one doesn't already exist,
 * then returns it.
 *
 * @private
 * @function negotiator
 * @param    {Object} req - the request object
 * @returns  {Object}     a negotiator
 */
function negotiator(req) {
    var h = req.headers;

    if (!req._negotiator) {
        req._negotiator = new Negotiator({
            headers: {
                accept: h.accept || '*/*',
                'accept-encoding': h['accept-encoding'] || 'identity'
            }
        });
    }

    return req._negotiator;
}

///--- API

/**
 * Patch Request object and extends with extra functionalities
 *
 * @private
 * @function patch
 * @param    {http.IncomingMessage|http2.Http2ServerRequest} Request -
 *                                                           Server Request
 * @returns  {undefined} No return value
 */
function patch(Request) {
    /**
     * Wraps all of the node
     * [http.IncomingMessage](https://nodejs.org/api/http.html)
     * APIs, events and properties, plus the following.
     * @class Request
     * @extends http.IncomingMessage
     */

    ///--- Patches

    /**
     * Builds an absolute URI for the request.
     *
     * @private
     * @memberof Request
     * @instance
     * @function absoluteUri
     * @param    {String} path - a url path
     * @returns  {String} uri
     */
    Request.prototype.absoluteUri = function absoluteUri(path) {
        assert.string(path, 'path');

        var protocol = this.isSecure() ? 'https://' : 'http://';
        var hostname = this.headers.host;
        return url.resolve(protocol + hostname + this.path() + '/', path);
    };

    /**
     * Check if the Accept header is present, and includes the given type.
     * When the Accept header is not present true is returned.
     * Otherwise the given type is matched by an exact match, and then subtypes.
     *
     * @public
     * @memberof Request
     * @instance
     * @function accepts
     * @param    {String | String[]} types - an array of accept type headers
     * @returns  {Boolean} is accepteed
     * @example
     * <caption>
     * You may pass the subtype such as html which is then converted internally
     * to text/html using the mime lookup table:
     * </caption>
     * // Accept: text/html
     * req.accepts('html');
     * // => true
     *
     * // Accept: text/*; application/json
     * req.accepts('html');
     * req.accepts('text/html');
     * req.accepts('text/plain');
     * req.accepts('application/json');
     * // => true
     *
     * req.accepts('image/png');
     * req.accepts('png');
     * // => false
     */
    Request.prototype.accepts = function accepts(types) {
        if (typeof types === 'string') {
            types = [types];
        }

        types = types.map(function map(t) {
            assert.string(t, 'type');

            if (t.indexOf('/') === -1) {
                t = mime.getType(t);
            }
            return t;
        });

        negotiator(this);

        return this._negotiator.preferredMediaType(types);
    };

    /**
     * Checks if the request accepts the encoding type(s) specified.
     *
     * @public
     * @memberof Request
     * @instance
     * @function acceptsEncoding
     * @param    {String | String[]} types - an array of accept type headers
     * @returns  {Boolean} is accepted encoding
     */
    Request.prototype.acceptsEncoding = function acceptsEncoding(types) {
        if (typeof types === 'string') {
            types = [types];
        }

        assert.arrayOfString(types, 'types');

        negotiator(this);

        return this._negotiator.preferredEncoding(types);
    };

    /**
     * Returns the value of the content-length header.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getContentLength
     * @returns {Number} content length
     */
    Request.prototype.getContentLength = function getContentLength() {
        if (this._clen !== undefined) {
            return this._clen === false ? undefined : this._clen;
        }

        // We should not attempt to read and parse the body of an
        // Upgrade request, so force Content-Length to zero:
        if (this.isUpgradeRequest()) {
            return 0;
        }

        var len = this.header('content-length');

        if (!len) {
            this._clen = false;
        } else {
            this._clen = parseInt(len, 10);
        }

        return this._clen === false ? undefined : this._clen;
    };
    /**
     * Returns the value of the content-length header.
     * @public
     * @memberof Request
     * @instance
     * @function contentLength
     * @returns {Number}
     */
    Request.prototype.contentLength = Request.prototype.getContentLength;

    /**
     * Returns the value of the content-type header. If a content-type is not
     * set, this will return a default value of `application/octet-stream`.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getContentType
     * @returns {String} content type
     */
    Request.prototype.getContentType = function getContentType() {
        if (this._contentType !== undefined) {
            return this._contentType;
        }

        var index;
        var type = this.headers['content-type'];

        if (!type) {
            // RFC2616 section 7.2.1
            this._contentType = 'application/octet-stream';
        } else if ((index = type.indexOf(';')) === -1) {
            this._contentType = type;
        } else {
            this._contentType = type.substring(0, index);
        }

        // #877 content-types need to be case insensitive.
        this._contentType = this._contentType.toLowerCase();

        return this._contentType;
    };

    /**
     * Returns the value of the content-type header. If a content-type is not
     * set, this will return a default value of `application/octet-stream`
     * @public
     * @memberof Request
     * @instance
     * @function getContentType
     * @returns {String} content type
     */
    Request.prototype.contentType = Request.prototype.getContentType;

    /**
     * Returns a Date object representing when the request was setup.
     * Like `time()`, but returns a Date object.
     *
     * @public
     * @memberof Request
     * @instance
     * @function date
     * @returns  {Date} date when request began being processed
     */
    Request.prototype.date = function date() {
        return this._date;
    };

    /**
     * Retrieves the complete URI requested by the client.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getHref
     * @returns {String} URI
     */
    Request.prototype.getHref = function getHref() {
        return this.getUrl().href;
    };

    /**
     * Returns the full requested URL.
     * @public
     * @memberof Request
     * @instance
     * @function href
     * @returns {String}
     * @example
     * // incoming request is http://localhost:3000/foo/bar?a=1
     * server.get('/:x/bar', function(req, res, next) {
     *     console.warn(req.href());
     *     // => /foo/bar/?a=1
     * });
     */
    Request.prototype.href = Request.prototype.getHref;

    /**
     * Retrieves the request uuid. was created when the request was setup.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getId
     * @returns  {String} id
     */
    Request.prototype.getId = function getId() {
        if (this._id !== undefined) {
            return this._id;
        }

        this._id = uuid.v4();

        return this._id;
    };

    /**
     * Returns the request id. If a `reqId` value is passed in,
     * this will become the request’s new id. The request id is immutable,
     * and can only be set once. Attempting to set the request id more than
     * once will cause restify to throw.
     *
     * @public
     * @memberof Request
     * @instance
     * @function id
     * @param {String} reqId - request id
     * @returns {String} id
     */
    Request.prototype.id = function id(reqId) {
        var self = this;

        if (reqId) {
            if (self._id) {
                throw new Error(
                    'request id is immutable, cannot be set again!'
                );
            } else {
                assert.string(reqId, 'reqId');
                self._id = reqId;
                return self._id;
            }
        }

        return self.getId();
    };

    /**
     * Retrieves the cleaned up url path.
     * e.g., /foo?a=1  =>  /foo
     *
     * @private
     * @memberof Request
     * @instance
     * @function getPath
     * @returns  {String} path
     */
    Request.prototype.getPath = function getPath() {
        return this.getUrl().pathname;
    };

    /**
     * Returns the cleaned up requested URL.
     * @public
     * @memberof Request
     * @instance
     * @function getPath
     * @returns  {String}
     * @example
     * // incoming request is http://localhost:3000/foo/bar?a=1
     * server.get('/:x/bar', function(req, res, next) {
     *     console.warn(req.path());
     *     // => /foo/bar
     * });
     */
    Request.prototype.path = Request.prototype.getPath;

    /**
     * Returns the raw query string. Returns empty string
     * if no query string is found.
     *
     * @public
     * @memberof Request
     * @instance
     * @function getQuery
     * @returns  {String} query
     * @example
     * // incoming request is /foo?a=1
     * req.getQuery();
     * // => 'a=1'
     * @example
     * <caption>
     * If the queryParser plugin is used, the parsed query string is
     * available under the req.query:
     * </caption>
     * // incoming request is /foo?a=1
     * server.use(restify.plugins.queryParser());
     * req.query;
     * // => { a: 1 }
     */
    Request.prototype.getQuery = function getQuery() {
        // always return a string, because this is the raw query string.
        // if the queryParser plugin is used, req.query will provide an empty
        // object fallback.
        return this.getUrl().query || '';
    };

    /**
     * Returns the raw query string. Returns empty string
     * if no query string is found
     * @private
     * @memberof Request
     * @instance
     * @function query
     * @returns  {String}
     */
    Request.prototype.query = Request.prototype.getQuery;

    /**
     * The number of ms since epoch of when this request began being processed.
     * Like date(), but returns a number.
     *
     * @public
     * @memberof Request
     * @instance
     * @function time
     * @returns  {Number} time when request began being processed in epoch:
     *                    ellapsed milliseconds since
     *                    January 1, 1970, 00:00:00 UTC
     */
    Request.prototype.time = function time() {
        return this._date.getTime();
    };

    /**
     * returns a parsed URL object.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getUrl
     * @returns  {Object} url
     */
    Request.prototype.getUrl = function getUrl() {
        if (this._cacheURL !== this.url) {
            this._url = url.parse(this.url);
            this._cacheURL = this.url;
        }
        return this._url;
    };

    /**
     * Returns the accept-version header.
     *
     * @private
     * @memberof Request
     * @instance
     * @function getVersion
     * @returns  {String} version
     */
    Request.prototype.getVersion = function getVersion() {
        if (this._version !== undefined) {
            return this._version;
        }

        this._version =
            this.headers['accept-version'] ||
            this.headers['x-api-version'] ||
            '*';

        return this._version;
    };

    /**
     * Returns the accept-version header.
     * @public
     * @memberof Request
     * @instance
     * @function version
     * @returns  {String}
     */
    Request.prototype.version = Request.prototype.getVersion;

    /**
     * Returns the version of the route that matched.
     *
     * @private
     * @memberof Request
     * @instance
     * @function matchedVersion
     * @returns {String} version
     */
    Request.prototype.matchedVersion = function matchedVersion() {
        if (this._matchedVersion !== undefined) {
            return this._matchedVersion;
        } else {
            return this.version();
        }
    };

    /**
     * Get the case-insensitive request header key,
     * and optionally provide a default value (express-compliant).
     * Returns any header off the request. also, 'correct' any
     * correctly spelled 'referrer' header to the actual spelling used.
     *
     * @public
     * @memberof Request
     * @instance
     * @function header
     * @param    {String} key - the key of the header
     * @param    {String} [defaultValue] - default value if header isn't
     *                                   found on the req
     * @returns  {String} header value
     * @example
     * req.header('Host');
     * req.header('HOST');
     * req.header('Accept', '*\/*');
     */
    Request.prototype.header = function header(key, defaultValue) {
        assert.string(key, 'key');

        key = key.toLowerCase();

        if (key === 'referer' || key === 'referrer') {
            key = 'referer';
        }

        return this.headers[key] || defaultValue;
    };

    /**
     * Returns any trailer header off the request. Also, 'correct' any
     * correctly spelled 'referrer' header to the actual spelling used.
     *
     * @public
     * @memberof Request
     * @instance
     * @function trailer
     * @param    {String} name - the name of the header
     * @param    {String} value - default value if header isn't found on the req
     * @returns  {String} trailer value
     */
    Request.prototype.trailer = function trailer(name, value) {
        assert.string(name, 'name');
        name = name.toLowerCase();

        if (name === 'referer' || name === 'referrer') {
            name = 'referer';
        }

        return (this.trailers || {})[name] || value;
    };

    /**
     * Check if the incoming request contains the `Content-Type` header field,
     * and if it contains the given mime type.
     *
     * @public
     * @memberof Request
     * @instance
     * @function is
     * @param    {String} type - a content-type header value
     * @returns  {Boolean} is content-type header
     * @example
     * // With Content-Type: text/html; charset=utf-8
     * req.is('html');
     * req.is('text/html');
     * // => true
     *
     * // When Content-Type is application/json
     * req.is('json');
     * req.is('application/json');
     * // => true
     *
     * req.is('html');
     * // => false
     */
    Request.prototype.is = function is(type) {
        assert.string(type, 'type');

        var contentType = this.getContentType();
        var matches = true;

        if (!contentType) {
            return false;
        }

        if (type.indexOf('/') === -1) {
            type = mime.getType(type);
        }

        if (type.indexOf('*') !== -1) {
            type = type.split('/');
            contentType = contentType.split('/');
            matches &= type[0] === '*' || type[0] === contentType[0];
            matches &= type[1] === '*' || type[1] === contentType[1];
        } else {
            matches = contentType === type;
        }

        return matches;
    };

    /**
     * Check if the incoming request is chunked.
     *
     * @public
     * @memberof Request
     * @instance
     * @function isChunked
     * @returns  {Boolean} is chunked
     */
    Request.prototype.isChunked = function isChunked() {
        return this.headers['transfer-encoding'] === 'chunked';
    };

    /**
     * Check if the incoming request is kept alive.
     *
     * @public
     * @memberof Request
     * @instance
     * @function isKeepAlive
     * @returns  {Boolean} is keep alive
     */
    Request.prototype.isKeepAlive = function isKeepAlive() {
        if (this._keepAlive !== undefined) {
            return this._keepAlive;
        }

        if (this.headers.connection) {
            this._keepAlive = /keep-alive/i.test(this.headers.connection);
        } else {
            this._keepAlive = this.httpVersion === '1.0' ? false : true;
        }

        return this._keepAlive;
    };

    /**
     * Check if the incoming request is encrypted.
     *
     * @public
     * @memberof Request
     * @instance
     * @function isSecure
     * @returns  {Boolean} is secure
     */
    Request.prototype.isSecure = function isSecure() {
        if (this._secure !== undefined) {
            return this._secure;
        }

        this._secure = this.connection.encrypted ? true : false;
        return this._secure;
    };

    /**
     * Check if the incoming request has been upgraded.
     *
     * @public
     * @memberof Request
     * @instance
     * @function isUpgradeRequest
     * @returns  {Boolean} is upgraded
     */
    Request.prototype.isUpgradeRequest = function isUpgradeRequest() {
        if (this._upgradeRequest !== undefined) {
            return this._upgradeRequest;
        } else {
            return false;
        }
    };

    /**
     * Check if the incoming request is an upload verb.
     *
     * @public
     * @memberof Request
     * @instance
     * @function isUpload
     * @returns  {Boolean} is upload
     */
    Request.prototype.isUpload = function isUpload() {
        var m = this.method;
        return m === 'PATCH' || m === 'POST' || m === 'PUT';
    };

    /**
     * toString serialization
     *
     * @public
     * @memberof Request
     * @instance
     * @function toString
     * @returns  {String} serialized request
     */
    Request.prototype.toString = function toString() {
        var headers = '';
        var self = this;
        var str;

        Object.keys(this.headers).forEach(function forEach(k) {
            headers += sprintf('%s: %s\n', k, self.headers[k]);
        });

        str = sprintf(
            '%s %s HTTP/%s\n%s',
            this.method,
            this.url,
            this.httpVersion,
            headers
        );

        return str;
    };

    /**
     * Returns the user-agent header.
     *
     * @public
     * @memberof Request
     * @instance
     * @function userAgent
     * @returns  {String} user agent
     */
    Request.prototype.userAgent = function userAgent() {
        return this.headers['user-agent'];
    };

    /**
     * Start the timer for a request handler.
     * By default, restify uses calls this automatically for all handlers
     * registered in your handler chain.
     * However, this can be called manually for nested functions inside the
     * handler chain to record timing information.
     *
     * @public
     * @memberof Request
     * @instance
     * @function startHandlerTimer
     * @param    {String}    handlerName - The name of the handler.
     * @returns  {undefined} no return value
     * @example
     * <caption>
     * You must explicitly invoke
     * endHandlerTimer() after invoking this function. Otherwise timing
     * information will be inaccurate.
     * </caption>
     * server.get('/', function fooHandler(req, res, next) {
     *     vasync.pipeline({
     *         funcs: [
     *             function nestedHandler1(req, res, next) {
     *                 req.startHandlerTimer('nestedHandler1');
     *                 // do something
     *                 req.endHandlerTimer('nestedHandler1');
     *                 return next();
     *             },
     *             function nestedHandler1(req, res, next) {
     *                 req.startHandlerTimer('nestedHandler2');
     *                 // do something
     *                 req.endHandlerTimer('nestedHandler2');
     *                 return next();
     *
     *             }...
     *        ]...
     *     }, next);
     * });
     */
    Request.prototype.startHandlerTimer = function startHandlerTimer(
        handlerName
    ) {
        var self = this;

        // For nested handlers, we prepend the top level handler func name
        var name =
            self._currentHandler === handlerName
                ? handlerName
                : self._currentHandler + '-' + handlerName;

        if (!self._timerMap) {
            self._timerMap = {};
        }

        self._timerMap[name] = process.hrtime();

        if (self.dtrace) {
            dtrace._rstfy_probes['handler-start'].fire(function fire() {
                return [
                    self.serverName,
                    self._currentRoute, // set in server._run
                    name,
                    self._dtraceId
                ];
            });
        }
    };

    /**
     * End the timer for a request handler.
     * You must invoke this function if you called `startRequestHandler` on a
     * handler. Otherwise the time recorded will be incorrect.
     *
     * @public
     * @memberof Request
     * @instance
     * @function endHandlerTimer
     * @param    {String}    handlerName - The name of the handler.
     * @returns  {undefined} no return value
     */
    Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) {
        var self = this;

        // For nested handlers, we prepend the top level handler func name
        var name =
            self._currentHandler === handlerName
                ? handlerName
                : self._currentHandler + '-' + handlerName;

        if (!self.timers) {
            self.timers = [];
        }

        self._timerMap[name] = process.hrtime(self._timerMap[name]);
        self.timers.push({
            name: name,
            time: self._timerMap[name]
        });

        if (self.dtrace) {
            dtrace._rstfy_probes['handler-done'].fire(function fire() {
                return [
                    self.serverName,
                    self._currentRoute, // set in server._run
                    name,
                    self._dtraceId
                ];
            });
        }
    };

    /**
     * Returns the connection state of the request. Current possible values are:
     * - `close` - when the request has been closed by the clien
     *
     * @public
     * @memberof Request
     * @instance
     * @function connectionState
     * @returns {String} connection state (`"close"`)
     */
    Request.prototype.connectionState = function connectionState() {
        var self = this;
        return self._connectionState;
    };

    /**
     * Returns true when connection state is "close"
     *
     * @private
     * @memberof Request
     * @instance
     * @function closed
     * @returns {Boolean} is closed
     */
    Request.prototype.closed = function closed() {
        var self = this;
        return self.connectionState() === 'close';
    };

    /**
     * Returns the route object to which the current request was matched to.
     *
     * @public
     * @memberof Request
     * @instance
     * @function getRoute
     * @returns {Object} route
     * @example
     * <caption>Route info object structure:</caption>
     * {
     *  path: '/ping/:name',
     *  method: 'GET',
     *  versions: [],
     *  name: 'getpingname'
     * }
     */
    Request.prototype.getRoute = function getRoute() {
        var self = this;
        return self.route;
    };
}

module.exports = patch;
