if typeof XMLHttpRequest isnt 'undefined' and
    (typeof window isnt 'undefined' or typeof self isnt 'undefined') and
    typeof navigator isnt 'undefined' and
    typeof navigator.userAgent is 'string'
  # Browser or Web Worker.
  if typeof XDomainRequest isnt 'undefined' and not
      ('withCredentials' of new XMLHttpRequest())
    DropboxXhrRequest = XDomainRequest
    DropboxXhrIeMode = true
    # IE's XDR doesn't allow setting requests' Content-Type to anything other
    # than text/plain, so it can't send _any_ forms.
    DropboxXhrCanSendForms = false
  else
    DropboxXhrRequest = XMLHttpRequest
    DropboxXhrIeMode = false
    # Web Workers don't support FormData at all.
    # Also, Firefox doesn't support adding named files to FormData.
    # https://bugzilla.mozilla.org/show_bug.cgi?id=690659
    DropboxXhrCanSendForms = typeof FormData isnt 'undefined' and
      navigator.userAgent.indexOf('Firefox') is -1
  DropboxXhrDoesPreflight = true
else
  # Node.js.
  DropboxXhrRequest = require('xhr2')  # Need an XHR emulation.
  DropboxXhrIeMode = false
  # Node.js doesn't have FormData. We wouldn't want to bother putting together
  # upload forms in node.js anyway, because it doesn't do CORS preflight
  # checks, so we can use PUT requests without a performance hit.
  DropboxXhrCanSendForms = false
  # Our XHR emulation skips CORS checks, which don't make sense for a server.
  DropboxXhrDoesPreflight = false

if typeof Uint8Array is 'undefined'
  # IE <= 9
  DropboxXhrArrayBufferView = null
  DropboxXhrWrapBlob = false
  DropboxXhrSendArrayBufferView = false
else
  # Old browsers don't expose the ArrayBufferView constructor.
  if Object.getPrototypeOf
    DropboxXhrArrayBufferView = Object.getPrototypeOf(
        Object.getPrototypeOf(new Uint8Array(0))).constructor
  else if Object.__proto__
    DropboxXhrArrayBufferView =
        (new Uint8Array(0)).__proto__.__proto__.constructor

  if typeof Blob is 'undefined'
    DropboxXhrWrapBlob = false
    DropboxXhrSendArrayBufferView = true
  else
    try
      if (new Blob [new Uint8Array(2)]).size is 2
        DropboxXhrWrapBlob = true
        DropboxXhrSendArrayBufferView = true
      else
        DropboxXhrSendArrayBufferView = false
        DropboxXhrWrapBlob = (new Blob [new ArrayBuffer(2)]).size is 2
    catch blobError
      DropboxXhrSendArrayBufferView = false
      DropboxXhrWrapBlob = false
      if typeof WebKitBlobBuilder isnt 'undefined'
        # Android's WebView doesn't support adding named files to FormData.
        if navigator.userAgent.indexOf('Android') isnt -1
          DropboxXhrCanSendForms = false

    if DropboxXhrArrayBufferView is Object
      # Browsers that haven't implemented XHR#send(ArrayBufferView) also don't
      # have a real ArrayBufferView prototype. (Safari, Firefox)
      DropboxXhrSendArrayBufferView = false

# Dispatches low-level AJAX calls (XMLHttpRequests).
class Dropbox.Xhr
  # The object used to perform AJAX requests (XMLHttpRequest).
  @Request = DropboxXhrRequest
  # Set to true when using the XDomainRequest API.
  @ieXdr = DropboxXhrIeMode
  # Set to true if the platform has proper support for FormData.
  @canSendForms = DropboxXhrCanSendForms
  # Set to true if the platform performs CORS preflight checks.
  @doesPreflight = DropboxXhrDoesPreflight
  # Superclass for all ArrayBufferView objects.
  @ArrayBufferView = DropboxXhrArrayBufferView
  # True if we think we can send ArrayBufferView objects via XHR.
  @sendArrayBufferView = DropboxXhrSendArrayBufferView
  # True if ArrayBuffer and ArrayBufferView instances get wrapped in Blobs
  # before sending via XHR.
  @wrapBlob = DropboxXhrWrapBlob

  # Sets up an AJAX request.
  #
  # @param {String} method the HTTP method used to make the request ('GET',
  #   'POST', 'PUT', etc.)
  # @param {String} baseUrl the URL that receives the request; this URL might
  #   be modified, e.g. by appending parameters for GET requests
  constructor: (@method, baseUrl) ->
    @isGet = @method is 'GET'
    @url = baseUrl
    @wantHeaders = false
    @headers = {}
    @params = null
    @body = null
    @preflight = not (@isGet or (@method is 'POST'))
    @signed = false
    @completed = false
    @responseType = null
    @callback = null
    @xhr = null
    @onError = null

  # @property {?XMLHttpRequest} the raw XMLHttpRequest object used to make the
  #   request; null until Dropbox.Xhr#prepare is called
  xhr: null

  # @property {?function(Dropbox.ApiError, function(Dropbox.ApiError))} if the
  #   XHR fails and this is non-null, it will be called with the error as its
  #   first argument; the function is responsible for calling its 2nd argument
  #   and passing it the ApiError, otherwise the Xhr's callback will not be
  #   called
  onError: null

  # Sets the parameters (form field values) that will be sent with the request.
  #
  # @param {?Object} params an associative array (hash) containing the HTTP
  #   request parameters
  # @return {Dropbox.Xhr} this, for easy call chaining
  setParams: (params) ->
    if @signed
      throw new Error 'setParams called after addOauthParams or addOauthHeader'
    if @params
      throw new Error 'setParams cannot be called twice'
    @params = params
    @

  # Sets the function called when the XHR completes.
  #
  # This function can also be set when calling Dropbox.Xhr#send.
  #
  # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
  #   the XHR completes; if an error occurs, the first parameter will be a
  #   Dropbox.ApiError instance; otherwise, the second parameter will be an
  #   instance of the required response type (e.g., String, Blob), and the
  #   third parameter will be the JSON-parsed 'x-dropbox-metadata' header
  # @return {Dropbox.Xhr} this, for easy call chaining
  setCallback: (@callback) ->
    @

  # Ammends the request parameters to include an OAuth signature.
  #
  # The OAuth signature will become invalid if the parameters are changed after
  # the signing process.
  #
  # This method automatically decides the best way to add the OAuth signature
  # to the current request. Modifying the request in any way (e.g., by adding
  # headers) might result in a valid signature that is applied in a sub-optimal
  # fashion. For best results, call this right before Dropbox.Xhr#prepare.
  #
  # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
  #   used to sign the request
  # @param {Boolean} cacheFriendly if true, the signing process choice will be
  #   biased towards allowing the HTTP cache to work; by default, the choice
  #   attempts to avoid the CORS preflight request whenever possible
  # @return {Dropbox.Xhr} this, for easy call chaining
  signWithOauth: (oauth, cacheFriendly) ->
    if Dropbox.Xhr.ieXdr
      @addOauthParams oauth
    else if @preflight or !Dropbox.Xhr.doesPreflight
      @addOauthHeader oauth
    else
      if @isGet and cacheFriendly
        @addOauthHeader oauth
      else
        @addOauthParams oauth

  # Ammends the request parameters to include an OAuth signature.
  #
  # The OAuth signature will become invalid if the parameters are changed after
  # the signing process.
  #
  # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
  #   used to sign the request
  # @return {Dropbox.Xhr} this, for easy call chaining
  addOauthParams: (oauth) ->
    if @signed
      throw new Error 'Request already has an OAuth signature'

    @params or= {}
    oauth.addAuthParams @method, @url, @params
    @signed = true
    @

  # Adds an Authorize header containing an OAuth signature.
  #
  # The OAuth signature will become invalid if the parameters are changed after
  # the signing process.
  #
  # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
  #   used to sign the request
  # @return {Dropbox.Xhr} this, for easy call chaining
  addOauthHeader: (oauth) ->
    if @signed
      throw new Error 'Request already has an OAuth signature'

    @params or= {}
    @signed = true
    @setHeader 'Authorization', oauth.authHeader(@method, @url, @params)

  # Sets the body (piece of data) that will be sent with the request.
  #
  # @param {String, Blob, ArrayBuffer} body the body to be sent in a request;
  #   GET requests cannot have a body
  # @return {Dropbox.Xhr} this, for easy call chaining
  setBody: (body) ->
    if @isGet
      throw new Error 'setBody cannot be called on GET requests'
    if @body isnt null
      throw new Error 'Request already has a body'

    if typeof body is 'string'
      # Content-Type will be set automatically.
    else if (typeof FormData isnt 'undefined') and (body instanceof FormData)
      # Content-Type will be set automatically.
    else
      @headers['Content-Type'] = 'application/octet-stream'
      @preflight = true

    @body = body
    @

  # Sends off an AJAX request and requests a custom response type.
  #
  # This method requires XHR Level 2 support, which is not available in IE
  # versions <= 9. If these browsers must be supported, it is recommended to
  # check if typeof Uint8Array !== 'undefined'
  #
  # @param {String} responseType the value that will be assigned to the XHR's
  #   responseType property
  # @return {Dropbox.Xhr} this, for easy call chaining
  setResponseType: (@responseType) ->
    @

  # Sets the value of a custom HTTP header.
  #
  # Custom HTTP headers require a CORS preflight in browsers, so requests that
  # use them will take more time to complete, especially on high-latency mobile
  # connections.
  #
  # @param {String} headerName the name of the HTTP header
  # @param {String} value the value that the header will be set to
  # @return {Dropbox.Xhr} this, for easy call chaining
  setHeader: (headerName, value) ->
    if @headers[headerName]
      oldValue = @headers[headerName]
      throw new Error "HTTP header #{headerName} already set to #{oldValue}"
    if headerName is 'Content-Type'
      throw new Error 'Content-Type is automatically computed based on setBody'
    @preflight = true
    @headers[headerName] = value
    @

  # Requests that the response headers be reported to the callback.
  #
  # Response headers are not returned by default because the parsing is
  # non-trivial and produces many intermediate strings.
  #
  # Response headers are not available on Internet Explorer 9 and below.
  #
  # @return {Dropbox.Xhr} this, for easy call chaining
  reportResponseHeaders: ->
    @wantHeaders = true

  # Simulates having an <input type="file"> being sent with the request.
  #
  # @param {String} fieldName the name of the form field / parameter (not of
  #   the uploaded file)
  # @param {String} fileName the name of the uploaded file (not the name of the
  #   form field / parameter)
  # @param {String, Blob, File} fileData contents of the file to be uploaded
  # @param {?String} contentType the MIME type of the file to be uploaded; if
  #   fileData is a Blob or File, its MIME type is used instead
  setFileField: (fieldName, fileName, fileData, contentType) ->
    if @body isnt null
      throw new Error 'Request already has a body'

    if @isGet
      throw new Error 'setFileField cannot be called on GET requests'

    if typeof(fileData) is 'object'
      if typeof ArrayBuffer isnt 'undefined'
        if fileData instanceof ArrayBuffer
          # Convert ArrayBuffer -> ArrayBufferView on standard-compliant
          # browsers, to avoid warnings from the Blob constructor.
          if Dropbox.Xhr.sendArrayBufferView
            fileData = new Uint8Array fileData
        else
          # Convert ArrayBufferView -> ArrayBuffer on older browsers, to avoid
          # having a Blob that contains "[object Uint8Array]" instead of the
          # actual data.
          if !Dropbox.Xhr.sendArrayBufferView and fileData.byteOffset is 0 and
             fileData.buffer instanceof ArrayBuffer
            fileData = fileData.buffer

      contentType or= 'application/octet-stream'
      try
        fileData = new Blob [fileData], type: contentType
      catch blobError
        # Stock Android / iPhone browsers don't implement the Blob contructor.
        # This code is only used on iPhone Safari / WebView (Cordova), because
        # Android's browser has a bug in sending Blobs.
        if window.WebKitBlobBuilder
          builder = new WebKitBlobBuilder
          builder.append fileData
          if blob = builder.getBlob contentType
            fileData = blob

      # Workaround for http://crbug.com/165095
      if typeof File isnt 'undefined' and fileData instanceof File
        fileData = new Blob [fileData], type: fileData.type
      useFormData = fileData instanceof Blob
    else
      useFormData = false

    if useFormData
      @body = new FormData()
      @body.append fieldName, fileData, fileName
    else
      contentType or= 'application/octet-stream'
      boundary = @multipartBoundary()
      @headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
      @body = ['--', boundary, "\r\n",
               'Content-Disposition: form-data; name="', fieldName,
                   '"; filename="', fileName, "\"\r\n",
               'Content-Type: ', contentType, "\r\n",
               "Content-Transfer-Encoding: binary\r\n\r\n",
               fileData,
               "\r\n", '--', boundary, '--', "\r\n"].join ''

  # @private
  # @return {String} a nonce suitable for use as a part boundary in a multipart
  #   MIME message
  multipartBoundary: ->
    [Date.now().toString(36), Math.random().toString(36)].join '----'

  # Moves this request's parameters to its URL.
  #
  # @private
  # @return {Dropbox.Xhr} this, for easy call chaining
  paramsToUrl: ->
    if @params
      queryString = Dropbox.Xhr.urlEncode @params
      if queryString.length isnt 0
        @url = [@url, '?', queryString].join ''
      @params = null
    @

  # Moves this request's parameters to its body.
  #
  # @private
  # @return {Dropbox.Xhr} this, for easy call chaining
  paramsToBody: ->
    if @params
      if @body isnt null
        throw new Error 'Request already has a body'
      if @isGet
        throw new Error 'paramsToBody cannot be called on GET requests'
      @headers['Content-Type'] = 'application/x-www-form-urlencoded'
      @body = Dropbox.Xhr.urlEncode @params
      @params = null
    @

  # Sets up an XHR request.
  #
  # This method completely sets up a native XHR object and stops short of
  # calling its send() method, so the API client has a chance of customizing
  # the XHR. After customizing the XHR, Dropbox.Xhr#send should be called.
  #
  #
  # @return {Dropbox.Xhr} this, for easy call chaining
  prepare: ->
    ieXdr = Dropbox.Xhr.ieXdr
    if @isGet or @body isnt null or ieXdr
      @paramsToUrl()
      if @body isnt null and typeof @body is 'string'
        @headers['Content-Type'] = 'text/plain; charset=utf8'
    else
      @paramsToBody()

    @xhr = new Dropbox.Xhr.Request()
    if ieXdr
      @xhr.onload = => @onXdrLoad()
      @xhr.onerror = => @onXdrError()
      @xhr.ontimeout = => @onXdrError()
      # NOTE: there are reports that XHR somtimes fails if onprogress doesn't
      #       have any handler
      @xhr.onprogress = ->
    else
      @xhr.onreadystatechange = => @onReadyStateChange()
    @xhr.open @method, @url, true

    unless ieXdr
      for own header, value of @headers
        @xhr.setRequestHeader header, value

    if @responseType
      if @responseType is 'b'
        if @xhr.overrideMimeType
          @xhr.overrideMimeType 'text/plain; charset=x-user-defined'
      else
        @xhr.responseType = @responseType

    @

  # Fires off the prepared XHR request.
  #
  # Dropbox.Xhr#prepare should be called exactly once before this method.
  #
  # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
  #   the XHR completes; if an error occurs, the first parameter will be a
  #   Dropbox.ApiError instance; otherwise, the second parameter will be an
  #   instance of the required response type (e.g., String, Blob), and the
  #   third parameter will be the JSON-parsed 'x-dropbox-metadata' header
  # @return {Dropbox.Xhr} this, for easy call chaining
  send: (callback) ->
    @callback = callback or @callback

    if @body isnt null
      body = @body
      if Dropbox.Xhr.sendArrayBufferView
        # Standards-compliant browsers don't like to send() naked ArrayBuffers
        if body instanceof ArrayBuffer
          body = new Uint8Array body
      else
        # Convert ArrayBufferView -> ArrayBuffer on older browsers, because
        # they will send "[object Uint8Array]" instead of the actual data.
        if body.byteOffset is 0 and body.buffer instanceof ArrayBuffer
          body = body.buffer

      try
        @xhr.send body
      catch xhrError
        # Node.js doesn't implement Blob.
        if !Dropbox.Xhr.sendArrayBufferView and Dropbox.Xhr.wrapBlob
          # Firefox doesn't support sending ArrayBufferViews.
          body = new Blob [body], type: 'application/octet-stream'
          @xhr.send body
        else
          throw xhrError
    else
      @xhr.send()
    @

  # Encodes an associative array (hash) into a x-www-form-urlencoded String.
  #
  # For consistency, the keys are sorted in alphabetical order in the encoded
  # output.
  #
  # @param {Object} object the JavaScript object whose keys will be encoded
  # @return {String} the object's keys and values, encoded using
  #   x-www-form-urlencoded
  @urlEncode: (object) ->
    chunks = []
    for key, value of object
      chunks.push @urlEncodeValue(key) + '=' + @urlEncodeValue(value)
    chunks.sort().join '&'

  # Encodes an object into a x-www-form-urlencoded key or value.
  #
  # @param {Object} object the object to be encoded; the encoding calls
  #   toString() on the object to obtain its string representation
  # @return {String} encoded string, suitable for use as a key or value in an
  #   x-www-form-urlencoded string
  @urlEncodeValue: (object) ->
    encodeURIComponent(object.toString()).replace(/\!/g, '%21').
      replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').
      replace(/\*/g, '%2A')

  # Decodes an x-www-form-urlencoded String into an associative array (hash).
  #
  # @param {String} string the x-www-form-urlencoded String to be decoded
  # @return {Object} an associative array whose keys and values are all strings
  @urlDecode: (string) ->
    result = {}
    for token in string.split '&'
      kvp = token.split '='
      result[decodeURIComponent(kvp[0])] = decodeURIComponent kvp[1]
    result

  # Handles the XHR readystate event.
  onReadyStateChange: ->
    return true if @xhr.readyState isnt 4  # XMLHttpRequest.DONE is 4

    # WebKit might fire this multiple times.
    #   http://crbug.com/159827
    return true if @completed
    @completed = true

    if @xhr.status < 200 or @xhr.status >= 300
      apiError = new Dropbox.ApiError @xhr, @method, @url
      if @onError
        @onError apiError, @callback
      else
        @callback apiError
      return true

    if @wantHeaders
      allHeaders = @xhr.getAllResponseHeaders()
      if allHeaders
        headers = Dropbox.Xhr.parseResponseHeaders allHeaders
      else
        # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=608735
        headers = @guessResponseHeaders()
      metadataJson = headers['x-dropbox-metadata']
    else
      headers = undefined
      metadataJson = @xhr.getResponseHeader 'x-dropbox-metadata'
    if metadataJson?.length
      try
        metadata = JSON.parse metadataJson
      catch jsonError
        # Make sure the app doesn't crash if the server goes crazy.
        metadata = undefined
    else
      metadata = undefined

    if @responseType
      if @responseType is 'b'
        dirtyText = if @xhr.responseText?
          @xhr.responseText
        else
          @xhr.response
        bytes = []
        for i in [0...dirtyText.length]
          bytes.push String.fromCharCode(dirtyText.charCodeAt(i) & 0xFF)
        text = bytes.join ''
        @callback null, text, metadata, headers
      else
        @callback null, @xhr.response, metadata, headers
      return true

    text = if @xhr.responseText? then @xhr.responseText else @xhr.response
    switch @xhr.getResponseHeader('Content-Type')
       when 'application/x-www-form-urlencoded'
         @callback null, Dropbox.Xhr.urlDecode(text), metadata, headers
       when 'application/json', 'text/javascript'
         @callback null, JSON.parse(text), metadata, headers
       else
          @callback null, text, metadata, headers
    true

  # Parses a block of raw HTTP headers.
  #
  # @private
  # Called by XHR's response processing code.
  #
  # @param {String} allHeaders the return value of an getAllResponseHeaders()
  #   call on a XMLHttpRequest object
  # @return {Object<String, String>} object whose keys are the lowercased HTTP
  #   header names, and whose values are the corresponding HTTP header values
  @parseResponseHeaders: (allHeaders) ->
    headers = {}
    headerLines = allHeaders.split "\n"
    for line in headerLines
      # NOTE: IE8 doesn't support trim(); we don't implement a fallback because
      #       XDR (used on IE < 10) doesn't support headers, so this won't get
      #       called anyway
      colonIndex = line.indexOf ':'
      name = line.substring(0, colonIndex).trim().toLowerCase()
      value = line.substring(colonIndex + 1).trim()
      headers[name] = value
    headers

  # Emulates getAllResponseHeaders()+parseResponseHeaders() on buggy browsers.
  #
  # @private
  # Called by XHR's response processing code.
  #
  # @return {Object<String, String>} object whose keys are the lowercased HTTP
  #   header names, and whose values are the corresponding HTTP header values
  guessResponseHeaders: ->
    # TODO(pwnall): investigate removing this when Firefox 21 gets released.
    headers = {}
    # Using ther header names listed at
    #     http://www.w3.org/TR/cors/#simple-response-header
    # and the names used by the Dropbox API server in
    # access-control-expose-headers.
    for name in ['cache-control', 'content-language', 'content-range',
                 'content-type', 'expires', 'last-modified', 'pragma',
                 'x-dropbox-metadata']
      value = @xhr.getResponseHeader name
      headers[name] = value if value
    headers

  # Handles the XDomainRequest onload event. (IE 8, 9)
  onXdrLoad: ->
    # WebKit fires onreadystatechange multiple times, might as well include the
    # same fix in IE-specific code.
    return true if @completed
    @completed = true

    text = @xhr.responseText
    if @wantHeaders
      headers = 'content-type': @xhr.contentType
    else
      headers = undefined

    metadata = undefined

    if @responseType
      @callback null, text, metadata, headers
      return true

    switch @xhr.contentType
     when 'application/x-www-form-urlencoded'
       @callback null, Dropbox.Xhr.urlDecode(text), metadata, headers
     when 'application/json', 'text/javascript'
       @callback null, JSON.parse(text), metadata, headers
     else
        @callback null, text, metadata, headers
    true

  # Handles the XDomainRequest onload event. (IE 8, 9)
  onXdrError: ->
    # WebKit fires onreadystatechange multiple times, might as well include the
    # same fix in IE-specific code.
    return true if @completed
    @completed = true

    apiError = new Dropbox.ApiError @xhr, @method, @url
    if @onError
      @onError apiError, @callback
    else
      @callback apiError
    return true
