Unirest = function (method, uri, headers, body, callback) {
  var unirest = function (uri, headers, body, callback) {
    var $this = {
      /**
       * Stream Multipart form-data request
       *
       * @type {Boolean}
       */
      _stream: false,

      /**
       * Container to hold multipart form data for processing upon request.
       *
       * @type {Array}
       * @private
       */
      _multipart: [],

      /**
       * Container to hold form data for processing upon request.
       *
       * @type {Array}
       * @private
       */
      _form: [],

      /**
       * Request option container for details about the request.
       *
       * @type {Object}
       */
      options: {
        /**
         * Url obtained from request method arguments.
         *
         * @type {String}
         */
        url: uri,

        /**
         * Method obtained from request method arguments.
         *
         * @type {String}
         */
        method: method,

        /**
         * List of headers with case-sensitive fields.
         *
         * @type {Object}
         */
        headers: {}
      },

      hasHeader: function (name) {
        var headers
        var lowercaseHeaders

        name = name.toLowerCase()
        headers = Object.keys($this.options.headers)
        lowercaseHeaders = headers.map(function (header) {
          return header.toLowerCase()
        })

        for (var i = 0; i < lowercaseHeaders.length; i++) {
          if (lowercaseHeaders[i] === name) {
            return headers[i]
          }
        }

        return false
      },

      /**
       * Turn on multipart-form streaming
       *
       * @return {Object}
       */
      stream: function () {
        $this._stream = true
        return this
      },

      /**
       * Attaches a field to the multipart-form request, with pre-processing.
       *
       * @param  {String} name
       * @param  {String} value
       * @return {Object}
       */
      field: function (name, value, options) {
        return handleField(name, value, options)
      },

      /**
       * Attaches a file to the multipart-form request.
       *
       * @param  {String} name
       * @param  {String|Object} path
       * @return {Object}
       */
      attach: function (name, path, options) {
        options = options || {}
        options.attachment = true
        return handleField(name, path, options)
      },

      /**
       * Attaches field to the multipart-form request, with no pre-processing.
       *
       * @param  {String} name
       * @param  {String|Object} path
       * @param  {Object} options
       * @return {Object}
       */
      rawField: function (name, value, options) {
        $this._multipart.push({
          name: name,
          value: value,
          options: options,
          attachment: options.attachment || false
        })
      },

      /**
       * Basic Header Authentication Method
       *
       * Supports user being an Object to reflect Request
       * Supports user, password to reflect SuperAgent
       *
       * @param  {String|Object} user
       * @param  {String} password
       * @param  {Boolean} sendImmediately
       * @return {Object}
       */
      auth: function (user, password, sendImmediately) {
        $this.options.auth = (is(user).a(Object)) ? user : {
          user: user,
          password: password,
          sendImmediately: sendImmediately
        }

        return $this
      },

      /**
       * Sets header field to value
       *
       * @param  {String} field Header field
       * @param  {String} value Header field value
       * @return {Object}
       */
      header: function (field, value) {
        if (is(field).a(Object)) {
          for (var key in field) {
            if (Object.prototype.hasOwnProperty.call(field, key)) {
              $this.header(key, field[key])
            }
          }

          return $this
        }

        var existingHeaderName = $this.hasHeader(field)
        $this.options.headers[existingHeaderName || field] = value

        return $this
      },

      /**
       * Serialize value as querystring representation, and append or set on `Request.options.url`
       *
       * @param  {String|Object} value
       * @return {Object}
       */
      query: function (value) {
        if (is(value).a(Object)) value = Unirest.serializers.form(value)
        if (!value.length) return $this
        $this.options.url += (does($this.options.url).contain('?') ? '&' : '?') + value
        return $this
      },

      /**
       * Set _content-type_ header with type passed through `mime.getType()` when necessary.
       *
       * @param  {String} type
       * @return {Object}
       */
      type: function (type) {
        $this.header('Content-Type', does(type).contain('/')
          ? type
          : mime.getType(type))
        return $this
      },

      /**
       * Data marshalling for HTTP request body data
       *
       * Determines whether type is `form` or `json`.
       * For irregular mime-types the `.type()` method is used to infer the `content-type` header.
       *
       * When mime-type is `application/x-www-form-urlencoded` data is appended rather than overwritten.
       *
       * @param  {Mixed} data
       * @return {Object}
       */
      send: function (data) {
        var type = $this.options.headers[$this.hasHeader('content-type')]

        if ((is(data).a(Object) || is(data).a(Array)) && !Buffer.isBuffer(data)) {
          if (!type) {
            $this.type('form')
            type = $this.options.headers[$this.hasHeader('content-type')]
            $this.options.body = Unirest.serializers.form(data)
          } else if (~type.indexOf('json')) {
            $this.options.json = true

            if ($this.options.body && is($this.options.body).a(Object)) {
              for (var key in data) {
                if (Object.prototype.hasOwnProperty.call(data, key)) {
                  $this.options.body[key] = data[key]
                }
              }
            } else {
              $this.options.body = data
            }
          } else {
            $this.options.body = Unirest.Request.serialize(data, type)
          }
        } else if (is(data).a(String)) {
          if (!type) {
            $this.type('form')
            type = $this.options.headers[$this.hasHeader('content-type')]
          }

          if (type === 'application/x-www-form-urlencoded') {
            $this.options.body = $this.options.body
              ? $this.options.body + '&' + data
              : data
          } else {
            $this.options.body = ($this.options.body || '') + data
          }
        } else {
          $this.options.body = data
        }

        return $this
      },

      /**
       * Takes multipart options and places them on `options.multipart` array.
       * Transforms body when an `Object` or _content-type_ is present.
       *
       * Example:
       *
       *      Unirest.get('http://google.com').part({
       *        'content-type': 'application/json',
       *        body: {
       *          phrase: 'Hello'
       *        }
       *      }).part({
       *        'content-type': 'application/json',
       *        body: {
       *          phrase: 'World'
       *        }
       *      }).end(function (response) {})
       *
       * @param  {Object|String} options When an Object, headers should be placed directly on the object,
       *                                 not under a child property.
       * @return {Object}
       */
      part: function (options) {
        if (!$this._multipart) {
          $this.options.multipart = []
        }

        if (is(options).a(Object)) {
          if (options['content-type']) {
            var type = Unirest.type(options['content-type'], true)
            if (type) options.body = Unirest.Response.parse(options.body)
          } else {
            if (is(options.body).a(Object)) {
              options.body = Unirest.serializers.json(options.body)
            }
          }

          $this.options.multipart.push(options)
        } else {
          $this.options.multipart.push({
            body: options
          })
        }

        return $this
      },

      /**
       * Instructs the Request to be retried if specified error status codes (4xx, 5xx, ETIMEDOUT) are returned.
       * Retries are delayed with an exponential backoff.
       *
       * @param {(err: Error) => boolean} [callback] - Invoked on response error. Return false to stop next request.
       * @param {Object} [options] - Optional retry configuration to override defaults.
       * @param {number} [options.attempts=3] - The number of retry attempts.
       * @param {number} [options.delayInMs=250] - The delay in milliseconds (delayInMs *= delayMulti)
       * @param {number} [options.delayMulti=2] - The multiplier of delayInMs after each attempt.
       * @param {Array<string|number>} [options.statusCodes=["ETIMEDOUT", "5xx"]] - The status codes to retry on.
       * @return {Object}
       */
      retry: function (callback, options) {

        $this.options.retry = {
          callback: typeof callback === "function" ? callback : null,
          attempts: options && +options.attempts || 3,
          delayInMs: options && +options.delayInMs || 250,
          delayMulti: options && +options.delayMulti || 2,
          statusCodes: (options && options.statusCodes || ["ETIMEDOUT", "5xx"]).slice(0)
        };

        return $this
      },

      /**
       * Proxies the call to end. This adds support for using promises as well as async/await.
       *
       * @param  {Function} callback
       * @return {Promise}
       **/
      then: function (callback) {
        return new Promise((resolve, reject) => {
          this.end(result => {
            try {
              resolve(callback(result))
            } catch (err) {
              reject(err)
            }
          })
        })
      },

      /**
       * Sends HTTP Request and awaits Response finalization. Request compression and Response decompression occurs here.
       * Upon HTTP Response post-processing occurs and invokes `callback` with a single argument, the `[Response](#response)` object.
       *
       * @param  {Function} callback
       * @return {Object}
       */
      end: function (callback) {
        var self = this
        var Request
        var header
        var parts
        var form

        function handleRetriableRequestResponse (result) {

          // If retries is not defined or all attempts tried, return true to invoke end's callback.
          if ($this.options.retry === undefined || $this.options.retry.attempts === 0) {
            return true
          }

          // If status code is not listed, abort with return true to invoke end's callback.
          var isStatusCodeDefined = (function (code, codes) {

            if (codes.indexOf(code) !== -1) {
              return true
            }

            return codes.reduce(function (p, c) {
                return p || String(code).split("").every(function (ch, i) {
                  return ch === "x" || ch === c[i]
                })
              }, false)

          }(result.code || result.error && result.error.code, $this.options.retry.statusCodes))

          if (!isStatusCodeDefined) {
            return true
          }

          if ($this.options.retry.callback) {
            var isContinue = $this.options.retry.callback(result)
            // If retry callback returns false, stop retries and invoke end's callback.
            if (isContinue === false) {
              return true;
            }
          }

          setTimeout(function () {
            self.end(callback)
          }, $this.options.retry.delayInMs)

          $this.options.retry.attempts--
          $this.options.retry.delayInMs *= $this.options.retry.delayMulti

          // Return false to not invoke end's callback.
          return false
        }

        function handleRequestResponse (error, response, body) {
          var result = {}
          var status
          var data
          var type

          // Handle pure error
          if (error && !response) {
            result.error = error

            if (handleRetriableRequestResponse(result) && callback) {
              callback(result)
            }

            return
          }

          // Handle No Response...
          // This is weird.
          if (!response) {
            console.log('This is odd, report this action / request to: http://github.com/mashape/unirest-nodejs')

            result.error = {
              message: 'No response found.'
            }

            if (handleRetriableRequestResponse(result) && callback) {
              callback(result)
            }

            return
          }

          // Create response reference
          result = response

          // Create response status reference
          status = response.statusCode

          // Normalize MSIE response to HTTP 204
          status = (status === 1223 ? 204 : status)

          // Obtain status range typecode (1, 2, 3, 4, 5, etc.)
          type = status / 100 | 0

          // Generate sugar helper properties for status information
          result.code = status
          result.status = status
          result.statusType = type
          result.info = type === 1
          result.ok = type === 2
          result.clientError = type === 4
          result.serverError = type === 5
          result.error = (type === 4 || type === 5) ? (function generateErrorMessage () {
            var msg = 'got ' + result.status + ' response'
            var err = new Error(msg)
            err.status = result.status
            return err
          })() : false

          // Iterate over Response Status Codes and generate more sugar
          for (var name in Unirest.Response.statusCodes) {
            result[name] = Unirest.Response.statusCodes[name] === status
          }

          // Cookie Holder
          result.cookies = {}

          // Cookie Sugar Method
          result.cookie = function (name) {
            return result.cookies[name]
          }

          function setCookie (cookie) {
            var crumbs = Unirest.trim(cookie).split('=')
            var key = Unirest.trim(crumbs[0])
            var value = Unirest.trim(crumbs.slice(1).join('='))

            if (crumbs[0] && crumbs[0] !== '') {
              result.cookies[key] = value === '' ? true : value
            }
          }

          if (response.cookies && is(response.cookies).a(Object) && Object.keys(response.cookies).length > 0) {
            result.cookies = response.cookies
          } else {
            // Handle cookies to be set
            var cookies = response.headers['set-cookie']
            if (cookies && is(cookies).a(Array)) {
              for (var index = 0; index < cookies.length; index++) {
                var entry = cookies[index]

                if (is(entry).a(String) && does(entry).contain(';')) {
                  entry.split(';').forEach(setCookie)
                }
              }
            }

            // Handle cookies that have been set
            cookies = response.headers.cookie
            if (cookies && is(cookies).a(String)) {
              cookies.split(';').forEach(setCookie)
            }
          }

          // Obtain response body
          body = body || response.body
          result.raw_body = body
          result.headers = response.headers

          // Handle Response Body
          if (body) {
            type = Unirest.type(result.headers['content-type'], true)
            if (type) data = Unirest.Response.parse(body, type)
            else data = body
          }

          result.body = data

          ;(handleRetriableRequestResponse(result)) && (callback) && callback(result)
        }

        function handleGZIPResponse (response) {
          if (/^(deflate|gzip)$/.test(response.headers['content-encoding'])) {
            var unzip = zlib.createUnzip()
            var stream = new Stream()
            var _on = response.on
            var decoder

            // Keeping node happy
            stream.req = response.req

            // Make sure we emit prior to processing
            unzip.on('error', function (error) {
              // Catch the parser error when there is no content
              if (error.errno === zlib.Z_BUF_ERROR || error.errno === zlib.Z_DATA_ERROR) {
                stream.emit('end')
                return
              }

              stream.emit('error', error)
            })

            // Start the processing
            response.pipe(unzip)

            // Ensure encoding is captured
            response.setEncoding = function (type) {
              decoder = new StringDecoder(type)
            }

            // Capture decompression and decode with captured encoding
            unzip.on('data', function (buffer) {
              if (!decoder) return stream.emit('data', buffer)
              var string = decoder.write(buffer)
              if (string.length) stream.emit('data', string)
            })

            // Emit yoself
            unzip.on('end', function () {
              stream.emit('end')
            })

            response.on = function (type, next) {
              if (type === 'data' || type === 'end') {
                stream.on(type, next)
              } else if (type === 'error') {
                _on.call(response, type, next)
              } else {
                _on.call(response, type, next)
              }
            }
          }
        }

        function handleFormData (form) {
          for (var i = 0; i < $this._multipart.length; i++) {
            var item = $this._multipart[i]

            if (item.attachment && is(item.value).a(String)) {
              if (does(item.value).contain('http://') || does(item.value).contain('https://')) {
                item.value = Unirest.request(item.value)
              } else {
                item.value = fs.createReadStream(path.resolve(item.value))
              }
            }

            form.append(item.name, item.value, item.options)
          }

          return form
        }

        if ($this._multipart.length && !$this._stream) {
          header = $this.options.headers[$this.hasHeader('content-type')]
          parts = URL.parse($this.options.url)
          form = new FormData()

          if (header) {
            $this.options.headers['content-type'] = header.split(';')[0] + '; boundary=' + form.getBoundary()
          } else {
            $this.options.headers['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary()
          }

          function authn(auth) {
              if (!auth) return null;
              if (typeof auth === 'string') return auth;
              if (auth.user && auth.pass) return auth.user + ':' + auth.pass;
              return auth;
          }

          return handleFormData(form).submit({
            protocol: parts.protocol,
            port: parts.port,
            // Formdata doesn't expect port to be included with host
            // so we use hostname rather than host
            host: parts.hostname,
            path: parts.path,
            method: $this.options.method,
            headers: $this.options.headers,
            auth: authn($this.options.auth || parts.auth)
          }, function (error, response) {
            var decoder = new StringDecoder('utf8')

            if (error) {
              return handleRequestResponse(error, response)
            }

            if (!response.body) {
              response.body = ''
            }

            // Node 10+
            response.resume()

            // GZIP, Feel me?
            handleGZIPResponse(response)

            // Fallback
            response.on('data', function (chunk) {
              if (typeof chunk === 'string') response.body += chunk
              else response.body += decoder.write(chunk)
            })

            // After all, we end up here
            response.on('end', function () {
              return handleRequestResponse(error, response)
            })
          })
        }

        Request = Unirest.request($this.options, handleRequestResponse)
        Request.on('response', handleGZIPResponse)

        if ($this._multipart.length && $this._stream) {
          handleFormData(Request.form())
        }

        return Request
      }
    }

    /**
     * Alias for _.header_
     * @type {Function}
     */
    $this.headers = $this.header

    /**
     * Alias for _.header_
     *
     * @type {Function}
     */
    $this.set = $this.header

    /**
     * Alias for _.end_
     *
     * @type {Function}
     */
    $this.complete = $this.end

    /**
     * Aliases for _.end_
     *
     * @type {Object}
     */

    $this.as = {
      json: $this.end,
      binary: $this.end,
      string: $this.end
    }

    /**
     * Handles Multipart Field Processing
     *
     * @param {String} name
     * @param {Mixed} value
     * @param {Object} options
     */
    function handleField (name, value, options) {
      var serialized
      var length
      var key
      var i

      options = options || { attachment: false }

      if (is(name).a(Object)) {
        for (key in name) {
          if (Object.prototype.hasOwnProperty.call(name, key)) {
            handleField(key, name[key], options)
          }
        }
      } else {
        if (is(value).a(Array)) {
          for (i = 0, length = value.length; i < length; i++) {
            serialized = handleFieldValue(value[i])
            if (serialized) {
              $this.rawField(name, serialized, options)
            }
          }
        } else if (value != null) {
          $this.rawField(name, handleFieldValue(value), options)
        }
      }

      return $this
    }

    /**
     * Handles Multipart Value Processing
     *
     * @param {Mixed} value
     */
    function handleFieldValue (value) {
      if (!(value instanceof Buffer || typeof value === 'string')) {
        if (is(value).a(Object)) {
          if (value instanceof fs.FileReadStream) {
            return value
          } else {
            return Unirest.serializers.json(value)
          }
        } else {
          return value.toString()
        }
      } else return value
    }

    function setupOption (name, ref) {
      $this[name] = function (arg) {
        $this.options[ref || name] = arg
        return $this
      }
    }

    // Iterates over a list of option methods to generate the chaining
    // style of use you see in Superagent and jQuery.
    for (var x in Unirest.enum.options) {
      if (Object.prototype.hasOwnProperty.call(Unirest.enum.options, x)) {
        var option = Unirest.enum.options[x]
        var reference = null

        if (option.indexOf(':') > -1) {
          option = option.split(':')
          reference = option[1]
          option = option[0]
        }

        setupOption(option, reference)
      }
    }

    if (headers && typeof headers === 'function') {
      callback = headers
      headers = null
    } else if (body && typeof body === 'function') {
      callback = body
      body = null
    }

    if (headers) $this.set(headers)
    if (body) $this.send(body)

    return callback ? $this.end(callback) : $this
  }

  return uri ? unirest(uri, headers, body, callback) : unirest
}
Initialize our Rest Container

@type {Object}