import uri, cgi, tables, logging, strutils, re, options

import jester/private/utils

when useHttpBeast:
  import httpbeast except Settings
  import options, httpcore

  type
    NativeRequest* = httpbeast.Request
else:
  import asynchttpserver

  type
    NativeRequest* = asynchttpserver.Request

type
  Request* = object
    req: NativeRequest
    patternParams: Option[Table[string, string]]
    reMatches: array[MaxSubpatterns, string]
    settings*: Settings

proc body*(req: Request): string =
  ## Body of the request, only for POST.
  ##
  ## You're probably looking for ``formData``
  ## instead.
  when useHttpBeast:
    req.req.body.get("")
  else:
    req.req.body

proc headers*(req: Request): HttpHeaders =
  ## Headers received with the request.
  ## Retrieving these is case insensitive.
  when useHttpBeast:
    if req.req.headers.isNone:
      newHttpHeaders()
    else:
      req.req.headers.get()
  else:
    req.req.headers

proc path*(req: Request): string =
  ## Path of request without the query string.
  when useHttpBeast:
    let p = req.req.path.get("")
    let queryStart = p.find('?')
    if unlikely(queryStart != -1):
      return p[0 .. queryStart-1]
    else:
      return p
  else:
    let u = req.req.url
    return u.path

proc reqMethod*(req: Request): HttpMethod =
  ## Request method, eg. HttpGet, HttpPost
  when useHttpBeast:
    req.req.httpMethod.get()
  else:
    req.req.reqMethod
proc reqMeth*(req: Request): HttpMethod {.deprecated.} =
  req.reqMethod

proc ip*(req: Request): string =
  ## IP address of the requesting client.
  when useHttpBeast:
    result = req.req.ip
  else:
    result = req.req.hostname

  let headers = req.headers
  if headers.hasKey("REMOTE_ADDR"):
    result = headers["REMOTE_ADDR"]
  if headers.hasKey("x-forwarded-for"):
    result = headers["x-forwarded-for"]

proc params*(req: Request): Table[string, string] =
  ## Parameters from the pattern and the query string.
  if req.patternParams.isSome():
    result = req.patternParams.get()
  else:
    result = initTable[string, string]()

  when useHttpBeast:
    let query = req.req.path.get("").parseUri().query
  else:
    let query = req.req.url.query

  try:
    for key, val in cgi.decodeData(query):
      result[key] = val
  except CgiError:
    logging.warn("Incorrect query. Got: $1" % [query])

  let contentType = req.headers.getOrDefault("Content-Type")
  if contentType.startswith("application/x-www-form-urlencoded"):
    try:
      parseUrlQuery(req.body, result)
    except:
      logging.warn("Could not parse URL query.")

proc formData*(req: Request): MultiData =
  let contentType = req.headers.getOrDefault("Content-Type")
  if contentType.startsWith("multipart/form-data"):
    result = parseMPFD(contentType, req.body)

proc matches*(req: Request): array[MaxSubpatterns, string] =
  req.reMatches

proc secure*(req: Request): bool =
  if req.headers.hasKey("x-forwarded-proto"):
    let proto = req.headers["x-forwarded-proto"]
    case proto.toLowerAscii()
    of "https":
      result = true
    of "http":
      result = false
    else:
      logging.warn("Unknown x-forwarded-proto ", proto)

proc port*(req: Request): int =
  if (let p = req.headers.getOrDefault("SERVER_PORT"); p != ""):
    result = p.parseInt
  else:
    result = if req.secure: 443 else: 80

proc host*(req: Request): string =
  req.headers.getOrDefault("HOST")

proc appName*(req: Request): string =
  ## This is set by the user in ``run``, it is
  ## overriden by the "SCRIPT_NAME" scgi
  ## parameter.
  req.settings.appName

proc stripAppName(path, appName: string): string =
  result = path
  if appname.len > 0:
    var slashAppName = appName
    if slashAppName[0] != '/' and path[0] == '/':
      slashAppName = '/' & slashAppName

    if path.startsWith(slashAppName):
      if slashAppName.len() == path.len:
        return "/"
      else:
        return path[slashAppName.len .. path.len-1]
    else:
      raise newException(ValueError,
          "Expected script name at beginning of path. Got path: " &
           path & " script name: " & slashAppName)

proc pathInfo*(req: Request): string =
  ## This is ``.path`` without ``.appName``.
  req.path.stripAppName(req.appName)

# TODO: Can cookie keys be duplicated?
proc cookies*(req: Request): Table[string, string] =
  ## Cookies from the browser.
  if (let cookie = req.headers.getOrDefault("Cookie"); cookie != ""):
    result = parseCookies(cookie)
  else:
    result = initTable[string, string]()

#[ Protected procs ]#

proc initRequest*(req: NativeRequest, settings: Settings): Request {.inline.} =
  Request(
    req: req,
    settings: settings
  )

proc getNativeReq*(req: Request): NativeRequest =
  req.req

#[ Only to be used by our route macro. ]#
proc setPatternParams*(req: var Request, p: Table[string, string]) =
  req.patternParams = some(p)

proc setReMatches*(req: var Request, r: array[MaxSubpatterns, string]) =
  req.reMatches = r
