/**
 * HTTP(s) transport based on XMLHttpRequest
 * To use in nodeJS XMLHttpRequest should be defined in global like
 *
 *      global.XMLHttpRequest = require('xhr2')
 *
 * @module transport
 * @memberOf module:@unitybase/ub-pub
 */

/**
 * Result of xhr / get / post HTTP request
 * @typedef XHRResponse
 * @property {string|Object|ArrayBuffer} data response body:
 *   - if `Content-Type` of the response is `application/json` - an Object contains response body parsed using `JSON.parse`
 *   - if request is called with `{responseType: 'arraybuffer'}` option - response body as ArrayBuffer
 *   - string in other case
 *
 * @property {number} status response HTTP status code
 * @property {function} headers header getter function
 * @property {Object} config configuration object that was used for request
 */
/* global XMLHttpRequest */

const ubUtils = require('./utils')

function lowercase (str) {
  return (str || '').toLowerCase()
}

function parseHeaders (headers) {
  const parsed = {}
  let key, val, i

  if (!headers) {
    return parsed
  }

  headers.split('\n').forEach(function (line) {
    i = line.indexOf(':')
    key = lowercase(line.substr(0, i).trim())
    val = line.substr(i + 1).trim()

    if (key) {
      if (parsed[key]) {
        parsed[key] += ', ' + val
      } else {
        parsed[key] = val
      }
    }
  })

  return parsed
}

function headersGetter (headers) {
  let headersObj = typeof headers === 'object' ? headers : undefined
  return function (name) {
    if (!headersObj) {
      headersObj = parseHeaders(headers)
    }
    if (name) {
      return headersObj[lowercase(name)]
    }
    return headersObj
  }
}

function transformData (data, headers, fns) {
  if (typeof fns === 'function') {
    return fns(data, headers)
  }
  fns.forEach(function (fn) {
    data = fn(data, headers)
  })
  return data
}

function transformDataPromise (data, headers, fns) {
  let rPromise = Promise.resolve(data)
  if (typeof fns === 'function') {
    return rPromise.then(function (data) {
      return fns(data, headers)
    })
  }
  fns.forEach(function (fn) {
    rPromise = rPromise.then(function (data) {
      return fn(data, headers)
    })
  })
  return rPromise
}

function isSuccess (status) {
  return status >= 200 && status < 300
}

function forEach (obj, iterator, context) {
  const keys = Object.keys(obj)
  keys.forEach(function (key) {
    iterator.call(context, obj[key], key)
  })
  return keys
}

function forEachSorted (obj, iterator, context) {
  const keys = Object.keys(obj).sort()
  keys.forEach(function (key) {
    iterator.call(context, obj[key], key)
  })
  return keys
}

function buildUrl (url, params) {
  if (!params) return url
  const parts = []
  forEachSorted(params, function (value, key) {
    if (value == null) { // jshint ignore:line
      return
    }
    if (!Array.isArray(value)) {
      value = [value]
    }

    value.forEach(function (v) {
      if (typeof v === 'object') {
        v = JSON.stringify(v)
      }
      parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(v))
    })
  })
  return url + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&')
}

let __lastRequestData
let __lastRequestTime = Date.now()
let __lastRequestURL

/**
 * see docs in ub-pub main module
 * @private
 * @param {Object} requestConfig
 * @param {String} requestConfig.url
 * @param {String} [requestConfig.method]
 * @param {Object.<string|Object>} [requestConfig.params]
 * @param {String|Object} [requestConfig.data]
 * @param {Object} [requestConfig.headers]
 * @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformRequest]
 * @param {function(data, function)|Array.<function(data, function)>} [requestConfig.transformResponse]
 * @param  {Number|Promise} [requestConfig.timeout]
 * @param  {Boolean} [requestConfig.withCredentials]
 * @param  {String} [requestConfig.responseType]
 * @param {Function} [requestConfig.onProgress]
 * @returns {Promise<XHRResponse>}
 */
function xhr (requestConfig) {
  const defaults = xhrDefaults
  const config = {
    transformRequest: defaults.transformRequest,
    transformResponse: defaults.transformResponse
  }
  const mergeHeaders = function (config) {
    let defHeaders = defaults.headers
    const reqHeaders = ubUtils.apply({}, config.headers)
    let defHeaderName, lowercaseDefHeaderName, reqHeaderName

    const execHeaders = function (headers) {
      forEach(headers, function (headerFn, header) {
        if (typeof headerFn === 'function') {
          const headerContent = headerFn()
          if (headerContent) {
            headers[header] = headerContent
          } else {
            delete headers[header]
          }
        }
      })
    }

    defHeaders = ubUtils.apply({}, defHeaders.common, defHeaders[lowercase(config.method)])

    // execute if header value is function
    execHeaders(defHeaders)
    execHeaders(reqHeaders)

    // using for-in instead of forEach to avoid unnecessary iteration after header has been found
    // eslint-disable-next-line no-labels
    defaultHeadersIteration:
    for (defHeaderName in defHeaders) {
      lowercaseDefHeaderName = lowercase(defHeaderName)
      for (reqHeaderName in reqHeaders) {
        if (lowercase(reqHeaderName) === lowercaseDefHeaderName) {
          // eslint-disable-next-line no-labels
          continue defaultHeadersIteration
        }
      }
      reqHeaders[defHeaderName] = defHeaders[defHeaderName]
    }
    return reqHeaders
  }
  let headers = mergeHeaders(requestConfig)

  ubUtils.apply(config, requestConfig)
  config.headers = headers
  config.method = config.method ? config.method.toUpperCase() : 'GET'

  let promise

  const transformResponse = function (response) {
    return transformDataPromise(response.data, response.headers, config.transformResponse)
      .then(function (trdData) {
        response.data = trdData
        return isSuccess(response.status) ? response : Promise.reject(response)
      })
  }

  const serverRequest = function (config) {
    headers = config.headers
    const reqData = transformData(config.data, headersGetter(headers), config.transformRequest)
    const url = buildUrl(config.url, config.params)

    // strip content-type if data is undefined
    if (!config.data) {
      forEach(headers, function (value, header) {
        if (lowercase(header) === 'content-type') {
          delete headers[header]
        }
      })
    } else {
      if (!ubUtils.isNodeJS) {
        // prevent reiteration sending of the same request
        // for example if HTML button on the form got a focus, user press `space` and button is not
        // disabled inside `onclick` handler then quite the same requests the same we got a many-many same requests
        const prevReqTime = __lastRequestTime
        __lastRequestTime = Date.now()
        if ((__lastRequestURL === url) && (typeof reqData === 'string') && (__lastRequestData === reqData) && (__lastRequestTime - prevReqTime < 100)) {
          ubUtils.logError('Quite the same request repeated 2 or more times in the last 100ms (so called monkey request). The request is', reqData)
          throw new ubUtils.UBError('monkeyRequestsDetected')
        } else {
          __lastRequestData = reqData
          __lastRequestURL = url
        }
      }
    }

    if (!config.withCredentials && defaults.withCredentials) {
      config.withCredentials = defaults.withCredentials
    }
    if (config.timeout === undefined && defaults.timeout) {
      config.timeout = defaults.timeout
    }

    // send request
    return sendReq(config, url, reqData, headers).then(transformResponse, transformResponse)
  }

  promise = Promise.resolve(config)

  // build a promise chain with request interceptors first, then the request, and response interceptors
  interceptors.filter(function (interceptor) {
    return !!interceptor.request || !!interceptor.requestError
  }).map(function (interceptor) {
    return { success: interceptor.request, failure: interceptor.requestError }
  })
    .concat({ success: serverRequest })
    .concat(interceptors.filter(function (interceptor) {
      return !!interceptor.response || !!interceptor.responseError
    }).map(function (interceptor) {
      return { success: interceptor.response, failure: interceptor.responseError }
    })
    ).forEach(function (then) {
      promise = promise.then(then.success, then.failure)
    })

  return promise
}

/**
 * Allow Request reiteration, for example in case of request are repeated after re-auth
 */
xhr.allowRequestReiteration = function () {
  __lastRequestData = null
}

const CONTENT_TYPE_APPLICATION_JSON = { 'Content-Type': 'application/json;charset=utf-8' }
const FILE_AVAILABLE = typeof File !== 'undefined' // global.File is not available in NodeJS
/**
 * The default HTTP parameters for {xhr}
 * @property {Object} xhrDefaults
 * @property {Array<Function>} xhrDefaults.transformRequest request transformations
 * @property {Array<Function>} xhrDefaults.transformResponse response transformations
 * @property {Object} xhrDefaults.headers Default headers to apply to request (depending of method)
 * @property {Number} xhrDefaults.timeout Default timeout to apply to request
 */
const xhrDefaults = {
  transformRequest: function (data) {
    return !!data && (typeof data === 'object') && (!FILE_AVAILABLE || !(data instanceof File)) && !(data instanceof ArrayBuffer || data.buffer instanceof ArrayBuffer)
      ? JSON.stringify(data)
      : data
  },
  transformResponse: function (data, headers) {
    if (data && (typeof data === 'string') && (headers('content-type') || '').indexOf('json') >= 0) {
      data = JSON.parse(data)
    }
    return data
  },
  headers: {
    common: { Accept: '*/*' },
    post: CONTENT_TYPE_APPLICATION_JSON,
    put: CONTENT_TYPE_APPLICATION_JSON,
    patch: CONTENT_TYPE_APPLICATION_JSON
  },
  timeout: ubUtils.isReactNative ? 5000 : 120000 // do not freeze ReactNative app by long timeouts
}

/**
 * Interceptors array
 * @type {Array.<Object>}
 * @protected
 */
const interceptors = []

/**
 * Array of config objects for currently pending requests. This is primarily meant to be used for debugging purposes.
 * @type {Array.<Object>}
 * @protected
 */
const pendingRequests = []

const XHR = XMLHttpRequest
function sendReq (config, url, reqData, reqHeaders) {
  return new Promise(function (resolve, reject) {
    let xhr = new XHR()
    const aborted = -1
    let status, timeoutId

    pendingRequests.push(config)

    xhr.open(config.method, url, true)
    forEach(reqHeaders /* MPV config.headers */, function (value, key) {
      if (value) {
        xhr.setRequestHeader(key, value)
      }
    })

    xhr.onreadystatechange = function () {
      if (xhr && xhr.readyState === 4) {
        let response, responseHeaders
        if (status !== aborted) {
          responseHeaders = xhr.getAllResponseHeaders()
          // responseText is the old-school way of retrieving response (supported by IE8 & 9)
          // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
          response = xhr.responseType ? xhr.response : xhr.responseText
        }

        // cancel timeout and subsequent timeout promise resolution
        if (timeoutId) {
          clearTimeout(timeoutId)
        }
        status = status || xhr.status
        xhr = null

        // normalize status, including accounting for IE bug (http://bugs.jquery.com/ticket/1450)
        status = Math.max(status === 1223 ? 204 : status, 0)

        const idx = pendingRequests.indexOf(config)
        if (idx !== -1) {
          pendingRequests.splice(idx, 1)
        }

        (isSuccess(status) ? resolve : reject)({
          data: response,
          status: status,
          headers: headersGetter(responseHeaders),
          config: config
        })
      }
    }

    if (config.onProgress) {
      if (xhr.upload) {
        xhr.upload.onprogress = config.onProgress
      } else {
        xhr.onprogress = config.onProgress
      }
    }

    if (config.withCredentials) {
      xhr.withCredentials = true
    }

    if (config.responseType) {
      xhr.responseType = config.responseType
    }

    xhr.send(reqData || null)

    if (config.timeout > 0) {
      timeoutId = setTimeout(function () {
        status = aborted
        if (xhr) {
          xhr.abort()
        }
      }, config.timeout)
    }
  })
}

/**
 * see docs in ub-pub main module
 * @private
 * @param {string} url
 * @param {Object=} [config]
 * @returns {Promise}
 */
function get (url, config) {
  return xhr(ubUtils.apply(config || {}, {
    method: 'GET',
    url: url
  }))
}

/**
 * see docs in ub-pub main module
 * @private
 * @param {string} url
 * @param {*} data
 * @param {Object=} [config]
 * @returns {Promise}
 */
function post (url, data, config) {
  return xhr(ubUtils.apply(config || {}, {
    method: 'POST',
    url: url,
    data: data
  }))
}

module.exports = {
  interceptors,
  pendingRequests,
  xhrDefaults,
  xhr,
  get,
  post
}