//
// Copyright (c) 2006, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   15 Mar 06  Andy Frank  Creation
//

using inet

**
** WebReq encapsulates a web request.
**
** See [pod doc]`pod-doc#webReq`.
**
abstract class WebReq
{

  **
  ** The HTTP request method in uppercase. Example: GET, POST, PUT.
  **
  abstract Str method()

  **
  ** Return if the method is GET
  **
  abstract Bool isGet()

  **
  ** Return if the method is POST
  **
  abstract Bool isPost()

  **
  ** The HTTP version of the request.
  **
  abstract Version version()

  **
  ** Get the IP host address of the client socket making this request.
  **
  abstract IpAddr remoteAddr()

  **
  ** Get the IP port of the client socket making this request.
  **
  abstract Int remotePort()

  **
  ** The request URI including the query string relative to
  ** this authority.  Also see `absUri`, `modBase`, and `modRel`.
  **
  ** Examples:
  **   /a/b/c
  **   /a?q=bar
  **
  abstract Uri uri()

  **
  ** The absolute request URI including the full authority
  ** and the query string.  Also see `uri`, `modBase`, and `modRel`.
  ** This method is equivalent to:
  **   "http://" + headers["Host"] + uri
  **
  ** Examples:
  **   http://www.foo.com/a/b/c
  **   http://www.foo.com/a?q=bar
  **
  virtual once Uri absUri()
  {
    host := headers["Host"]
    if (host == null) throw Err("Missing Host header")
    return `http://${host}/` + uri
  }

  **
  ** Get the WebMod which is currently responsible
  ** for processing this request.
  **
  abstract WebMod mod

  **
  ** Base URI of the current WebMod.  This Uri always ends in a slash.
  ** This is the URI used to route to the WebMod itself.  The remainder
  ** of `uri` is stored in `modRel` so that the following always
  ** holds true (with exception of a trailing slash):
  **   modBase + modRel == uri
  **
  ** For example if the current WebMod is mounted as '/mod' then:
  **   uri          modBase   modRel
  **   ----------   -------   -------
  **   `/mod`       `/mod/`   ``
  **   `/mod/`      `/mod/`   ``
  **   `/mod?q`     `/mod/`   `?q`
  **   `/mod/a`     `/mod/`   `a`
  **   `/mod/a/b`   `/mod/`   `a/b`
  **
  Uri modBase := `/`
  {
    set
    {
      if (!it.isDir) throw ArgErr("modBase must end in slash");
      if (it.path.size > uri.path.size) throw ArgErr("modBase ($it) is not slice of uri ($uri)");
      &modBase = it
      modRelVal = uri.relTo(&modBase)//[it.path.size..-1]
    }
  }

  **
  ** WebMod relative part of the URI - see `modBase`.
  **
  Uri modRel() { modRelVal ?: uri }
  private Uri? modRelVal

  **
  ** Map of HTTP request headers.  The headers map is readonly
  ** and case insensitive (see `sys::Map.caseInsensitive`).
  **
  ** Examples:
  **   req.headers["Accept-Language"]
  **
  abstract Str:Str headers()

  **
  ** Get the accepted locales for this request based on the
  ** "Accept-Language" HTTP header.  List is sorted by preference, where
  ** 'locales.first' is best, and 'locales.last' is worst.  This list is
  ** guarenteed to contain Locale("en").
  **
  virtual once Locale[] locales()
  {
    list := Locale[,]
    hval := headers["Accept-Language"]
    if (hval != null)
    {
      WebUtil.parseList(hval).each |val|
      {
        try
        {
          colon := val.index(";q=")
          qual  := colon==null ? 1f : val[colon+3..-1].toFloat
          lang  := colon==null ? val : val[0..<colon]
          loc   := Locale.fromStr(lang, false)
          if (qual > 0f && loc != null && !list.contains(loc)) list.add(loc)
        }
        catch (Err err) { err.trace }
      }
    }

    // make sure we always have 'en'
    en := Locale("en")
    if (!list.contains(en)) list.add(en)
    return list
  }

  **
  ** Map of cookie values keyed by cookie name.  The
  ** cookies map is readonly and case insensitive.
  **
  virtual once Str:Str cookies()
  {
    try
      return MimeType.parseParams(headers.get("Cookie", "")).ro
    catch (Err e)
      e.trace
    return Str:Str[:].ro
  }

  **
  ** Get the session associated with this browser "connection".
  ** The session must be accessed the first time before the
  ** response is committed.
  **
  abstract WebSession session()

  **
  ** Get the key/value pairs of the form data.  If the request
  ** content type is "application/x-www-form-urlencoded", then the
  ** first time this method is called the request content is read
  ** and parsed using `sys::Uri.decodeQuery`.  If the content
  ** type is not "application/x-www-form-urlencoded" this method
  ** returns null.
  **
  virtual once [Str:Str]? form()
  {
    ct := headers.get("Content-Type", "").lower
    if (ct.startsWith("application/x-www-form-urlencoded"))
    {
      len := headers["Content-Length"]
      if (len == null) throw IOErr("Missing Content-Length header")
      return Uri.decodeQuery(in.readLine(len.toInt))
    }
    return null
  }

  **
  ** Get the stream to read request body.  See `WebUtil.makeContentInStream`
  ** to check under which conditions request content is available.
  ** If request content is not available, then throw an exception.
  **
  ** If the client specified the "Expect: 100-continue" header, then the first
  ** access of the request input stream will automatically send the client
  ** a [100 Continue]`pod-doc#expectContinue` response.
  **
  abstract InStream in()

  **
  ** Access to socket options for this request.
  **
  abstract SocketOptions socketOptions()

  **
  ** Access to underlying socket - internal use only!
  **
  @NoDoc abstract TcpSocket socket()

  **
  ** Stash allows you to stash objects on the WebReq object
  ** in order to pass data b/w Weblets while processing
  ** this request.
  **
  virtual Str:Obj? stash() { stashRef }
  private Str:Obj? stashRef := Str:Obj?["web.startTime":Duration.now]

  **
  ** Given a web request:
  **   1. check that the content-type is form-data
  **   2. get the boundary string
  **   3. invoke callback for each part (see `WebUtil.parseMultiPart`)
  **
  ** For each part in the stream call the given callback function with
  ** the part's form name, headers, and an input stream used to read the
  ** part's body.
  **
  Void parseMultiPartForm(|Str formName, InStream in, [Str:Str] headers| cb)
  {
    mime := MimeType(this.headers["Content-Type"])
    if (mime.subType != "form-data") throw Err("Invalid content-type: $mime")
    boundary := mime.params["boundary"] ?: throw Err("Missing boundary param: $mime")
    WebUtil.parseMultiPart(this.in, boundary) |partHeaders, partIn|
    {
      cd := partHeaders["Content-Disposition"] ?: throw Err("Multi-part missing Content-Disposition")
      semi := cd.index(";") ?: throw Err("Expected semicolon; Content-Disposition: $cd")
      params := MimeType.parseParams(cd[cd.index(";")+1..-1])
      formName := params["name"] ?: throw Err("Expected name param; Content-Disposition: $cd")
      cb(formName, partIn, partHeaders)
      try { partIn.skip(Int.maxVal) } catch {} // drain stream
    }
  }

}