//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   17 Mar 08  Brian Frank  Creation
//   03 Aug 15  Matthew Giannini  RFC6265
//

**
** Cookie models an HTTP cookie used to pass data between the server
** and user agent as defined by [RFC 6265]`http://tools.ietf.org/html/rfc6265`.
**
** See `WebReq.cookies` and `WebRes.cookies`.
**
@Js
const class Cookie
{

  **
  ** Parse a HTTP cookie header name/value pair. The parsing of the name-value pair
  ** is done according to the algorithm outlined in [ยง 5.2]`http://tools.ietf.org/html/rfc6265#section-5.2`
  ** of the RFC.
  **
  ** Throw ParseErr or return null if not formatted correctly.
  **
  static Cookie? fromStr(Str s, Bool checked := true)
  {
    try
    {
      Str? params := null
      semi := s.index(";")
      if (semi != null)
      {
        params = s[semi+1..-1]
        s = s[0..<semi]
      }

      eq := s.index("=")
      if (eq == null) throw ParseErr(s)
      name := s[0..<eq].trim
      val := s[eq+1..-1].trim

      if (params == null) return make(name, val)

      return make(name, val)
      {
        p := MimeType.parseParams(params)
        it.domain = p["Domain"]
        it.path = p.get("Path", "/")
      }
    }
    catch (Err e)
    {
      if (checked) throw ParseErr("Cookie: $s")
      return null
    }
  }

  **
  ** Construct a cookie to use for session management.
  ** The following web config properties are used:
  **   - secureSessionCookie: force use of 'Secure' cookie option
  **   - sameSiteSessionCookie: force use of 'SameSite:strict' cookie option
  **
  @NoDoc static Cookie makeSession(Str name, Str val, [Field:Obj?]? overrides := null)
  {
    pod := Cookie#.pod
    fields := [
      #secure: pod.config("secureSessionCookie", "false") == "true",
      #sameSite: pod.config("sameSiteSessionCookie", "strict"),
      #httpOnly: true
    ]
    if (overrides != null) overrides.each |v, f| { fields[f] = v }
    return Cookie.make(name, val, Field.makeSetFunc(fields))
  }

  **
  ** Construct with name and value.  The name must be a valid
  ** HTTP token and must not start with "$" (see `WebUtil.isToken`).
  ** The value string must be an ASCII string within the inclusive
  ** range of 0x20 and 0x7e (see `WebUtil.toQuotedStr`) with the
  ** exception of the semicolon.
  **
  ** Fantom cookies will use quoted string values, however some browsers
  ** such as IE won't parse a quoted string with semicolons correctly,
  ** so we make semicolons illegal.  If you have a value which might
  ** include non-ASCII characters or semicolons, then consider encoding
  ** using something like Base64:
  **
  **   // write response
  **   res.cookies.add(Cookie("baz", val.toBuf.toBase64))
  **
  **   // read from request
  **   val := Buf.fromBase64(req.cookies.get("baz", "")).readAllStr
  **
  new make(Str name, Str val, |This|? f := null)
  {
    if (f != null) f(this)
    this.name = name
    this.val = val

    // validate name
    if (!WebUtil.isToken(this.name) || this.name[0] == '$')
      throw ArgErr("Cookie name has illegal chars: $val")

    // validate value
    if (!this.val.all |Int c->Bool| { return 0x20 <= c && c <= 0x7e && c != ';'})
      throw ArgErr("Cookie value has illegal chars: $val")
    if (this.val.size + 32 >= WebUtil.maxTokenSize) // fudge room for quotes & escapes
      throw ArgErr("Cookie value too big")
  }

  **
  ** Name of the cookie.
  **
  const Str name

  **
  ** Value string of the cookie.
  **
  const Str val

  **
  ** Defines the lifetime of the cookie, after the the max-age
  ** elapses the client should discard the cookie.  The duration
  ** is floored to seconds (fractional seconds are truncated).
  ** If maxAge is null (the default) then the  cookie persists
  ** until the client is shutdown.  If zero is specified, the
  ** cookie is discarded immediately.  Note that many browsers
  ** still don't recognize max-age, so setting max-age also
  ** always includes an expires attribute.
  **
  const Duration? maxAge

  **
  ** Specifies the domain for which the cookie is valid.
  ** An explicit domain must always start with a dot.  If
  ** null (the default) then the cookie only applies to
  ** the server which set it.
  **
  const Str? domain

  **
  ** Specifies the subset of URLs to which the cookie applies.
  ** If set to "/" (the default), then the cookie applies to all
  ** paths.  If the path is null, it as assumed to be the same
  ** path as the document being described by the header which
  ** contains the cookie.
  **
  const Str? path := "/"

  **
  ** If true, then the client only sends this cookie using a
  ** secure protocol such as HTTPS.  Defaults to false.
  **
  const Bool secure := false

  **
  ** If true, then the cookie is not available to JavaScript.
  ** Defaults to true.
  **
  const Bool httpOnly := true

  **
  ** If this value is non-null, then we add the SameSite attribute to
  ** the cookie. Valid values are
  **   - 'lax'
  **   - 'strict'
  ** By default we set the attribute to 'strict'
  **
  const Str? sameSite := "strict"

  **
  ** Return the cookie formatted as an Set-Cookie HTTP header.
  **
  override Str toStr()
  {
    s := StrBuf(64)
    s.add(name).add("=").add(val)
    if (maxAge != null)
    {
      // we need to use Max-Age *and* Expires since many browsers
      // such as Safari and IE still don't recognize max-age
      s.add(";Max-Age=").add(maxAge.toSec)
      if (maxAge.toMillis <= 0)
        s.add(";Expires=").add("Sat, 01 Jan 2000 00:00:00 GMT")
      else
        s.add(";Expires=").add((DateTime.nowUtc+maxAge).toHttpStr)
    }
    if (domain != null) s.add(";Domain=").add(domain)
    if (path != null) s.add(";Path=").add(path)
    if (secure) s.add(";Secure")
    if (httpOnly) s.add(";HttpOnly")
    if (sameSite != null) s.add(";SameSite=${sameSite}")
    return s.toStr
  }

  internal Str toNameValStr() { "$name=$val" }

}