content: ' /**
  '  * @module requests
  '  */
  
  ' /**
  '  * @memberof module:requests
  '  * @name Requests
  '  * @function
  '  * @description Main Class for Requests
  '  * @param {Dynamic} args - Associative array of args to pass into Alacrity
  
  function Requests() as Object
      return {
          request : Requests_request,
          get: Requests_getRequest,
          post: Requests_postRequest
      }
  end function
  
  function Requests_getRequest(url as String, args=invalid)
      return m.request("GET", url, args)
  end function
  
  function Requests_postRequest(url as String, args=invalid)
      return m.request("POST", url, args)
  end function
  
  function Requests_request(method, url as String, args as Object)
  
      _params = {}
      _headers = {}
      ' _cookies = invalid
      _data = invalid
      _json = invalid
      _timeout = 30000
      _retryCount = 0
      ' _allow_redirects = true
      _verify = "common:/certs/ca-bundle.crt"
      _useCache = true
      _cacheSeconds = invalid
      _parseJson = true
      _parseJsonFlags = ""
  
      if args <> invalid and type(args) = "roAssociativeArray"
          if args.params <> invalid and type(args.params) = "roAssociativeArray"
              _params = args.params
          end if
          if args.headers <> invalid and type(args.headers) = "roAssociativeArray"
              _headers = args.headers
          end if
          if args.data <> invalid and (type(args.data) = "String" or type(args.data) = "roString")
              _data = args.data
          end if
          if args.json <> invalid and type(args.json) = "roAssociativeArray"
              _json = FormatJson(args.json)
          end if
          if args.timeout <> invalid and (type(args.timeout) = "Integer" or type(args.timeout) = "roInteger")
              _timeout = args.timeout
          end if
          if args.retryCount <> invalid and (type(args.retryCount) = "Integer" or type(args.retryCount) = "roInteger")
              _retryCount = args.retryCount
          end if
          if args.verify <> invalid and (type(args.verify) = "String" or type(args.verify) = "roString")
              _verify = args.verify
          end if
          if args.useCache <> invalid and (type(args.useCache) = "Boolean" or type(args.useCache) = "roBoolean")
              _useCache = args.useCache
          end if
          if args.cacheSeconds <> invalid and (type(args.cacheSeconds) = "Integer" or type(args.cacheSeconds) = "roInteger")
              _cacheSeconds = args.cacheSeconds
          end if
          if args.parseJson <> invalid and (type(args.parseJson) = "Boolean" or type(args.parseJson) = "roBoolean")
              _parseJson = args.parseJson
          end if
          if args.parseJsonFlags <> invalid and (type(args.parseJsonFlags) = "String" or type(args.parseJsonFlags) = "roString")
              _parseJsonFlags = args.parseJsonFlags
          end if
      end if
  
      ' Constructs and sends a Request.
      ' :param method: method for the new Request object.
      ' :param url: URL for the new Request object.
      ' :param params: (optional) AA to append as a query string to the Request.
      ' :param data: (optional) String to send in the body of the Request.
      ' :param json: (optional) A JSON serializable object to send in the body of the Request.
      ' :param headers: (optional) AA of HTTP Headers to send with the Request.
      ' :param cookies: (optional) Dict or CookieJar object to send with the Request.
      ' :param timeout: (optional) How many seconds to wait for the server to send data
      '     before giving up, as a float
      ' :param retryCount: (optional) How many times to retry a failed response.
      ' :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection.
      ' :param verify: (optional) String to specify SSL cert bundle.
      '               If this is set to empty string `InitClientCertificates` is not called on roUrlTransfer.
      '               Defaults to `common:/certs/ca-bundle.crt`
      ' :param useCache: (optional) Boolean. Enable/Disable caching. Defaults to true
      ' :param cacheSeconds: (optional) Integer. Enable/Disable caching. Defaults to response's Cache-Control if it exists
      ' :param parseJson: (optional) Boolean. Enable/Disable parsing json response. Defaults to true
      ' :param parseJsonFlags: (optional) String. Flags passed to ParseJson() function
      ' :return: :class:`Requests <Response>` object
      ' Usage::
      '   req = Requests().get('http://httpbin.org/get')
  
  
      requestHeaders = Requests_headers()
      requestHeaders.addHeadersAA(_headers)
  
      requestQueryString = Requests_queryString()
      requestQueryString.addParamsAA(_params)
  
      ' Setup the data (we overwrite JSON if it's provided)
      if _data <> invalid
          data = _data
      else if _json <> invalid
          data = _json
          requestHeaders.addHeader("Content-Type", "application/json")
      else
          data = ""
      end if
  
      'TODO: Add Cookies Support
      'urlTransfer.EnableCookies()
      'urlTransfer.GetCookies(domain, path)
      'urlTransfer.AddCookies(cookies)
      'urlTransfer.ClearCookies()
  
      url = requestQueryString.append(url)
      headers = requestHeaders._headers
  
      rc = Requests_cache(method, url, headers)
  
      response = invalid
      if rc <> invalid and _useCache
          response = rc.get(_cacheSeconds)
      end if
  
      if response = invalid
          response = Requests_run(method, url, headers, data, _timeout, _retryCount, _verify, _parseJson, _parseJsonFlags)
          if rc <> invalid and _useCache and response.ok = true
              rc.put(response)
          end if
      end if
  
      return response
  
  end function
  
  function Requests_run(method, url, headers, data, timeout, retryCount, verify, parseJson, parseJsonFlags)
  
      urlTransfer = RequestsUrlTransfer(true, true, verify)
      urlTransfer.setUrl(url)
      urlTransfer.SetHeaders(headers)
  
      ? "[http] ------ START HTTP REQUEST ------"
  
      ? "[http] URL:", urlTransfer.GetURL()
  
      ? "[http] Timeout= ", timeout
  
      ? "[http] Headers: ",  headers
  
      cancel_and_return = false
  
      responseEvent = invalid
      requestDetails = {
          timesTried : 0,
          parseJson : parseJson,
          parseJsonFlags: parseJsonFlags
      }
      'while we still have try times
      while retryCount >= 0
  
          'deincrement the number of retries
          retryCount = retryCount - 1
          requestDetails.timesTried = requestDetails.timesTried + 1
  
          sent = invalid
  
          ? "[http] Method: ",  method
          if method="POST"
              sent = urlTransfer.AsyncPostFromString(data)
          else if method = "GET"
              sent = urlTransfer.AsyncGetToString()
          else if method = "HEAD"
              sent = urlTransfer.AsyncHead()
          else
              'PUT, PATCH, DELETE
              urlTransfer.SetRequest(method)
              sent = urlTransfer.AsyncPostFromString(data)
          end if
  
          if sent = true
              clock = CreateObject("roTimespan")
              timeout_call = clock.TotalMilliseconds() + timeout
  
              event = invalid
  
              while true and cancel_and_return = false
  
                  if m.top <> invalid
                      if m.top.quit <> invalid
                          cancel_and_return = m.top.quit
                      end if
                  end if
  
                  event = urlTransfer.GetPort().GetMessage()
  
                  if type(event) = "roUrlEvent"
                      exit while
                  end if
  
                  if clock.TotalMilliseconds() > timeout_call
                      exit while
                  end if
              end while
  
              if type(event) = "roUrlEvent"
                  responseEvent = event
                  responseCode = event.GetResponseCode()
                  ? "[http] Response Code", responseCode
                  if responseCode > 0 and responseCode < 400
                      'Response was good, so we break the while
                      exit while
                  else
                      'We have a bad response
                      ? "[http] Bad response", responseCode
                      ? "[http] Will Retry ", retryCount
                  end if
              else
                  if m.cancel_and_return = true
                      ? "[http] Killing the Task"
                      exit while
                  else
                      'We timed out so we should cancel the request
                      ? "[http] Event Timed Out"
                      urlTransfer.AsyncCancel()
                      'Exponential backoff timeouts
                      timeout = timeout * 2
                      ? "[http] Timeout=", timeout
                  end if
              end if
          end if
      end while
      ? "[http] ------ END HTTP REQUEST ------"
  
      return Requests_response(urlTransfer, responseEvent, requestDetails)
  
  end function
  
  
  
  function Requests_cache(method as String, url as String, headers as Object)
  
      if method <> "GET"
          return invalid
      end if
  
      cacheKey = url
      headersString = FormatJson(headers)
      if headersString <> invalid
          cacheKey = cacheKey + headersString
      end if
  
      ba = CreateObject("roByteArray")
      ba.FromAsciiString(cacheKey)
      digest = CreateObject("roEVPDigest")
      digest.Setup("md5")
      md5Key = digest.Process(ba)
  
      fileLocation = "cachefs:/" + md5Key
  
      return {
          _cacheKey: cacheKey,
          _md5Key: md5Key,
  
          location: fileLocation,
  
          exists: function() as Boolean
                  fs = CreateObject("roFileSystem")
                  if fs <> invalid
                      return fs.Exists(m.location)
                  end if
                  return false
              end function,
  
          get: function(expireSeconds) as Object
                  if m.exists()
                      fileData = ReadAsciiFile(m.location)
                      if fileData <> ""
                          dataSplit = fileData.Split(chr(10))
                          if dataSplit.count() = 2
                              fileTimeastamp = dataSplit[0].toInt()
                              date = CreateObject("roDateTime")
                              nowTimestamp = date.AsSeconds()
                              response = ParseJson(dataSplit[1])
                              if response <> invalid
                                  if expireSeconds = invalid and response.headers <> invalid
                                      cacheControl = response.headers["cache-control"]
                                      if cacheControl <> invalid
                                          cacheControlSplit = cacheControl.split(",")
                                          if cacheControlSplit.count() > 1
                                              cacheControlMaxAgeSplit = cacheControlSplit[1].split("=")
                                              if cacheControlMaxAgeSplit.count() > 1
                                                  expireSeconds = val(cacheControlMaxAgeSplit[1])
                                              end if
                                          end if
                                      end if
                                  end if
                                  if expireSeconds <> invalid
                                      if fileTimeastamp + expireSeconds >= nowTimestamp
                                          response.cacheHit = true
                                          return response
                                      end if
                                  end if
                              end if
                          end if
                      end if
                  end if
                  return invalid
              end function,
  
          put: function(response) as Boolean
                  date = CreateObject("roDateTime")
                  timestamp = date.AsSeconds()
                  putString = stri(timestamp) + chr(10)
                  jsonResponse = FormatJson(response)
  
                  if jsonResponse <> invalid
                      putString = putString + jsonResponse
                      return WriteAsciiFile(m.location, putString)
                  end if
                  return false
              end function,
  
          delete: function() as Boolean
                  fs = CreateObject("roFileSystem")
                  if fs <> invalid
                      return fs.Delete(m.location)
                  end if
                  return false
              end function
  
      }
  
  end function
  
  function Requests_headers()
  
      return {
          _headers: {},
          addHeader: sub(key as String, value as String)
                  m._headers[key] = value
              end sub,
  
          addHeadersAA: sub(headers as Object)
                  m._headers.Append(headers)
              end sub,
  
          build: m.get,
          get: m._headers
      }
  
  end function
  
  function Requests_queryString()
  
      return {
          _urlTransfer: CreateObject("roUrlTransfer"),
          _qs_array: [],
          addString: sub(params as String)
                  if Requests_Utils_inString("&", params)
                      split_params = params.split("&")
                      for each param in split_params
                          if Requests_Utils_inString("=", param)
                              split_param = param.split("=")
                              m.addParamKeyValue(split_param[0], split_param[1])
                          else
                              m.addParamKeyValue(param, "")
                          end if
                      end for
                  else if Requests_Utils_inString("=", params)
                      split_params = params.split("=")
                      m.addParamKeyValue(split_params[0], split_params[1])
                  else
                      m.addParamKeyValue(params, "")
                  end if
              end sub,
          addParamKeyValue: sub(param as String, key as String)
                  m._qs_array.push([param, key])
              end sub,
          addParamsAA: sub(params as Object)
                  for each item in params.Items()
                      m.addParamKeyValue(item.key, item.value)
                  end for
              end sub,
          addParamsArray: sub(params as Object)
                  if params.Count() > 0
                      for each item in params
                          if item.Count() > 1
                              m.addParamKeyValue(item[0], item[1])
                          else if item.Count() > 0
                              m.addParamKeyValue(item[0], "")
                          end if
                      end for
                  end if
              end sub,
  
          build: function() as String
              ' "build the QS output from the added params"
                  output = ""
                  c = 0
                  for each qs in m._qs_array
                      if c = 0
                          output = qs[0] + "=" + m._urlTransfer.Escape(qs[1])
                      else
                          output = output + "&" + qs[0] + "=" + m._urlTransfer.Escape(qs[1])
                      end if
                      c += 1
                  end for
                  return output
              end function,
  
          append: function(url as String) as String
              ' "append the QS on a provided URL"
                  if m._qs_array.Count() > 0
                      if Requests_Utils_inString("?", url)
                          if url.right(1) = "?"
                              return url + m.build()
                          else
                              return url + "&" + m.build()
                          end if
                      else
                          return url + "?" + m.build()
                      end if
                  end if
                  return url
              end function
      }
  
  end function
  
  function Requests_response(urlTransfer as Object, responseEvent as Object, requestDetails as Object)
  
      rr = {}
  
      rr.timesTried = requestDetails.timesTried
      rr.url = urlTransfer.GetUrl()
      rr.ok = false
      rr.cacheHit = false
  
      if responseEvent <> invalid
          rr.statusCode = responseEvent.GetResponseCode()
          rr.text = responseEvent.GetString()
          rr.headers = responseEvent.GetResponseHeaders()
          rr.headersArray = responseEvent.GetResponseHeadersArray()
  
          rr.GetSourceIdentity = responseEvent.GetSourceIdentity()
          rr.GetFailureReason = responseEvent.GetFailureReason()
          rr.target_ip = responseEvent.GetTargetIpAddress()
          if rr.statusCode > 0 and rr.statusCode < 400
              rr.ok = true
          end if
      end if
  
      if rr.text <> invalid
          if requestDetails.parseJson = true
              rr.json = parseJson(rr.text, requestDetails.parseJsonFlags)
          end if
          rr.body = rr.text
      end if
  
      return rr
  
  end function
  
  
  function RequestsUrlTransfer(enableEncodings as Boolean, retainBodyOnError as Boolean, verify as String)
  
      _urlTransfer = CreateObject("roUrlTransfer")
      _urlTransfer.SetPort(CreateObject("roMessagePort"))
  
       _urlTransfer.EnableEncodings(enableEncodings)
       _urlTransfer.RetainBodyOnError(retainBodyOnError)
      if verify <> ""
          _urlTransfer.SetCertificatesFile(verify)
          _urlTransfer.InitClientCertificates()
      end if
  
      return _urlTransfer
  
  end function
  function Requests_Utils_inString(char as String, strValue as String)
  
      for each single_char in strValue.split("")
          if single_char = char
              return true
          end if
      end for
  
      return false
  
  end function