//
// Copyright (c) 2007, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   27 Jun 07  Brian Frank  Creation
//

**
** WebUtil encapsulates several useful utility web methods.
** Also see `sys::MimeType` and its utility methods.
**
@Js
class WebUtil
{

//////////////////////////////////////////////////////////////////////////
// Chars
//////////////////////////////////////////////////////////////////////////

  **
  ** Return if the specified string is a valid HTTP token production
  ** which is any ASCII character which is not a control char or a
  ** separator.  The separators characters are:
  **   "(" | ")" | "<" | ">" | "@" |
  **   "," | ";" | ":" | "\" | <"> |
  **   "/" | "[" | "]" | "?" | "=" |
  **   "{" | "}" | SP | HT
  **
  static Bool isToken(Str s)
  {
    if (s.isEmpty) return false
    return s.all |Int c->Bool| { isTokenChar(c) }
  }

  **
  ** Return if given char unicode point is allowable within the
  ** HTTP token production.  See `isToken`.
  **
  static Bool isTokenChar(Int c)
  {
    c < 127 && tokenChars[c]
  }

  private static const Bool[] tokenChars
  static
  {
    m := Bool[,]
    for (i:=0; i<127; ++i) m.add(i > 0x20)
    m['(']  = false;  m[')'] = false;  m['<']  = false;  m['>'] = false
    m['@']  = false;  m[','] = false;  m[';']  = false;  m[':'] = false
    m['\\'] = false;  m['"'] = false;  m['/']  = false;  m['['] = false
    m[']']  = false;  m['?'] = false;  m['=']  = false;  m['{'] = false
    m['}']  = false;  m[' '] = false;  m['\t'] = false;
    tokenChars = m
  }

  **
  ** Return the specified string as a HTTP quoted string according
  ** to RFC 2616 Section 2.2.  The result is wrapped in quotes.  Throw
  ** ArgErr if any character is outside of the ASCII range of 0x20
  ** to 0x7e.  The quote char itself is backslash escaped.
  ** See `fromQuotedStr`.
  **
  static Str toQuotedStr(Str s)
  {
    buf := StrBuf()
    buf.addChar('"')
    s.each |Int c|
    {
      if (c < 0x20 || c > 0x7e) throw ArgErr("Invalid quoted str chars: $s")
      if (c == '"') buf.addChar('\\')
      buf.addChar(c)
    }
    buf.addChar('"')
    return buf.toStr
  }

  **
  ** Decode a HTTP quoted string according to RFC 2616 Section 2.2.
  ** The given string must be wrapped in quotes.  See `toQuotedStr`.
  **
  static Str fromQuotedStr(Str s)
  {
    if (s.size < 2 || s[0] != '"' || s[s.size-1] != '"')
      throw ArgErr("Not quoted str: $s")
    return s[1..-2].replace("\\\"", "\"")
  }

//////////////////////////////////////////////////////////////////////////
// Parsing
//////////////////////////////////////////////////////////////////////////

  **
  ** Parse a list of comma separated tokens.  Any leading
  ** or trailing whitespace is trimmed from the list of tokens.
  **
  static Str[] parseList(Str s)
  {
    return s.split(',')
  }

  **
  ** Parse a series of HTTP headers according to RFC 2616 section
  ** 4.2.  The final CRLF which terminates headers is consumed with
  ** the stream positioned immediately following.  The headers are
  ** returned as a [case insensitive]`sys::Map.caseInsensitive` map.
  ** Throw ParseErr if headers are malformed.
  **
  static Str:Str parseHeaders(InStream in) { doParseHeaders(in, null) }

  // handle set-cookie headers individually
  internal static Str:Str doParseHeaders(InStream in, Cookie[]? cookies)
  {
    headers := CaseInsensitiveMap<Str,Str>()
    //headers.caseInsensitive = true
    Str? last := null

    // read headers into map
    while (true)
    {
      peek := in.peek

      // CRLF is end of headers
      if (peek == CR) break

      // if line starts with space it is
      // continuation of last header field
      if (peek.isSpace && last != null)
      {
        headers[last] += " " + WebUtil.readLine(in).trim
        continue
      }

      // key/value pair
      key := token(in, ':').trim
      val := token(in, CR).trim
      if (in.read != LF)
        throw ParseErr("Invalid CRLF line ending")

      // set-cookie
      if (key.equalsIgnoreCase("Set-Cookie") && cookies != null)
      {
        cookie := Cookie.fromStr(val, false)
        if (cookie != null) cookies.add(cookie)
      }

      // check if key already defined in which case
      // this is an append, otherwise its a new pair
      dup := headers[key]
      if (dup == null)
        headers[key] = val
      else
        headers[key] = dup + "," + val
      last = key
    }

    // consume final CRLF
    if (in.read != CR || in.read != LF)
      throw ParseErr("Invalid CRLF headers ending")

    return headers
  }

  **
  ** Read the next token from the stream up to the specified
  ** separator. We place a limit of 512 bytes on a single token.
  ** Consume the separate char too.
  **
  private static Str token(InStream in, Int sep)
  {
    // read up to separator
    tok := in.readStrToken(maxTokenSize) |Int ch->Bool| { return ch == sep }

    // sanity checking
    if (tok == null) throw IOErr("Unexpected end of stream")
    if (tok.size >= maxTokenSize) throw ParseErr("Token too big")

    // read separator
    in.read

    return tok
  }

  **
  ** Given an HTTP header that uses q values, return a map of
  ** name/q-value pairs.  This map has a def value of 0.
  **
  ** Example:
  **   compress,gzip              =>  ["compress":1f, "gzip":1f]
  **   compress;q=0.5,gzip;q=0.0  =>  ["compress":0.5f, "gzip":0.0f]
  **
  static Str:Float parseQVals(Str s)
  {
    map := Str:Float[:]
    //map.def = 0.0f
    s.split(',').each |tok|
    {
      if (tok.isEmpty) return
      name := tok
      q    := 1.0f
      x := tok.index(";")
      if (x != null)
      {
        name = tok[0..<x].trim
        attrs := tok[x+1..-1].trim
        qattr := attrs.index("q=")
        if (qattr != null) {
          try
            q  = Float.fromStr(attrs[qattr+2..-1], true)
          catch
            q = 1.0f
        }
      }
      map[name] = q
    }
    return map
  }

  ** Write HTTP headers
  @NoDoc static Void writeHeaders(OutStream out, [Str:Str] headers)
  {
    headers.each |v,k|
    {
      if (v.containsChar('\n')) v = v.splitLines.join("\n ")
      out.print(k).print(": ").print(v).print("\r\n")
    }
  }

//////////////////////////////////////////////////////////////////////////
// IO
//////////////////////////////////////////////////////////////////////////

  **
  ** Given a set of HTTP headers map Content-Type to its charset
  ** or default to UTF-8.
  **
  static Charset headersToCharset([Str:Str] headers)
  {
    ct := headers["Content-Type"]
    if (ct != null)
    {
      try {
        MimeType(ct).charset
      }
      catch {}
    }
    return Charset.utf8
  }

  **
  ** Given a set of headers, wrap the specified input stream
  ** to read the content body:
  **   1. If Content-Encoding is 'gzip' then wrap via `sys::Zip.gzipInStream`
  **   2. If Content-Length then `makeFixedInStream`
  **   3. If Transfer-Encoding is chunked then `makeChunkedInStream`
  **   4. If Content-Type assume non-pipelined connection and
  **      return 'in' directly
  **
  ** If a stream is returned, then it is automatically configured
  ** with the correct content encoding based on the Content-Type.
  **
  static InStream makeContentInStream([Str:Str] headers, InStream in)
  {
    // handle Content-Length / Transfer-Encoding
    in = doMakeContentInStream(headers, in)

    // check for content-encoding
    ce := headers["Content-Encoding"]
    if (ce != null)
    {
      ce = ce.lower
      switch (ce)
      {
        case "gzip":    return Zip.gzipInStream(in)
        case "deflate": return Zip.deflateInStream(in)
        default: throw IOErr("Unsupported Content-Encoding: $ce")
      }
    }
    return in
  }

  private static InStream? doMakeContentInStream([Str:Str] headers, InStream in)
  {
    // map the "Content-Type" response header to the
    // appropiate charset or default to UTF-8.
    cs := headersToCharset(headers)

    // check for fixed content length
    len := headers["Content-Length"]
    if (len != null)
      return makeFixedInStream(in, len.toInt) { charset = cs }

    // check for chunked transfer encoding
    if (headers.get("Transfer-Encoding", "").lower.contains("chunked"))
      return makeChunkedInStream(in) { charset = cs }

    // assume open ended content until close
    return in
  }

  **
  ** Given a set of headers, wrap the specified output stream
  ** to write the content body:
  **   1. If Content-Length then `makeFixedOutStream`
  **   2. If Content-Type then set Transfer-Encoding header to
  **      chunked and return `makeChunkedOutStream`
  **   3. Assume no content and return null
  **
  ** If a stream is returned, then it is automatically configured
  ** with the correct content encoding based on the Content-Type.
  **
  static OutStream? makeContentOutStream([Str:Str] headers, OutStream out)
  {
    // map the "Content-Type" response header to the
    // appropiate charset or default to UTF-8.
    cs := headersToCharset(headers)

    // check for fixed content length
    len := headers["Content-Length"]
    if (len != null)
      return makeFixedOutStream(out, len.toInt) { charset = cs }

    // if content-type then assumed chunked output
    ct := headers["Content-Type"]
    if (ct != null)
    {
      headers["Transfer-Encoding"] = "chunked"
      return makeChunkedOutStream(out) { charset = cs }
    }

    // no content
    return null
  }

  **
  ** Wrap the given input stream to read a fixed number of bytes.
  ** Once 'fixed' bytes have been read from the underlying input
  ** stream, the wrapped stream will return end-of-stream.  Closing
  ** the wrapper stream does not close the underlying stream.
  **
  static InStream makeFixedInStream(InStream in, Int fixed)
  {
    return ChunkInStream(in, fixed)
  }

  **
  ** Wrap the given input stream to read bytes using a HTTP
  ** chunked transfer encoding.  The wrapped streams provides
  ** a contiguous stream of bytes until the last chunk is read.
  ** Closing the wrapper stream does not close the underlying stream.
  **
  static InStream makeChunkedInStream(InStream in)
  {
    return ChunkInStream(in, null)
  }

  **
  ** Wrap the given output stream to write a fixed number of bytes.
  ** Once 'fixed' bytes have been written, attempting to further
  ** bytes will throw IOErr.  Closing the wrapper stream does not
  ** close the underlying stream.
  **
  static OutStream makeFixedOutStream(OutStream out, Int fixed)
  {
    return FixedOutStream(out, fixed)
  }

  **
  ** Wrap the given output stream to write bytes using a HTTP
  ** chunked transfer encoding.  Closing the wrapper stream
  ** terminates the chunking, but does not close the underlying
  ** stream.
  **
  static OutStream makeChunkedOutStream(OutStream out)
  {
    return ChunkOutStream(out)
  }

  **
  ** Read line of HTTP protocol.  Raise exception if unexpected
  ** end of stream or the line exceeds our max size.
  **
  @NoDoc static Str readLine(InStream in)
  {
    max := 65536 // 64KB
    line := in.readLine(max)
    if (line == null) throw IOErr("Unexpected end of stream")
    if (line.size == max) throw IOErr("Max request line")
    return line
  }

//////////////////////////////////////////////////////////////////////////
// Multi-Part Forms
//////////////////////////////////////////////////////////////////////////

  **
  ** Parse a multipart/form-data input stream.  For each part in the
  ** stream call the given callback function with the part's headers
  ** and an input stream used to read the part's body.  Each callback
  ** must completely drain the input stream to prepare for the next
  ** part.  Also see `WebReq.parseMultiPartForm`.
  **
  static Void parseMultiPart(InStream in, Str boundary, |[Str:Str] headers, InStream in| cb)
  {
    boundary = "--" + boundary
    line := WebUtil.readLine(in)
    if (line == boundary + "--") return
    if (line != boundary) throw IOErr("Expecting boundry line $boundary.toCode")
    while (true)
    {
      headers := parseHeaders(in)
      partIn := MultiPartInStream(in, boundary)
      cb(headers, partIn)
      if (partIn.endOfParts) break
    }
  }

//////////////////////////////////////////////////////////////////////////
// JsMain
//////////////////////////////////////////////////////////////////////////

  **
  ** Generate the method invocation code used to boostrap into
  ** JavaScript from a webpage.  This *must* be called inside the
  ** '<head>' tag for the page.  The main method will be invoked
  ** using the 'onLoad' DOM event.
  **
  ** The 'main' argument can be either a type or method.  If no
  ** method is specified, 'main' is used.  If the method is not
  ** static, a new instance of type is created:
  **
  **   "foo::Instance"     =>  Instance().main()
  **   "foo::Instance.bar" =>  Instance().bar()
  **   "foo::Static"       =>  Static.main()
  **   "foo::Static.bar"   =>  Static.bar()
  **
  ** If 'env' is specified, then vars will be added to and available
  ** from `sys::Env.vars` on client-side.
  **
  @Deprecated { msg="use WebOutStream.initJs" }
  static Void jsMain(OutStream out, Str main, [Str:Str]? env := null)
  {
    envStr := StrBuf()
    if (env?.size > 0)
    {
      envStr.add("var env = fan.std.CaseInsensitiveMap.make();\n")
      //envStr.add("  env.caseInsensitive\$(true);\n")
      env.each |v,k|
      {
        envStr.add("  ")
        v = v.toCode('\'')
        // NOTE: uriPodBase is only used for FWT; and this now gets
        // configured via normal Env.var moving forward in initJs
        if (k == "sys.uriPodBase")
          envStr.add("fan.fwt.WidgetPeer.\$uriPodBase = $v;\n")
        else
          envStr.add("env.set('$k', $v);\n")
      }
      envStr.add("  fan.std.Env.cur().\$setVars(env);")
    }

    out.printLine(
     "<script type='text/javascript'>
      window.addEventListener('load', function()
      {
        // inject env vars
        $envStr.toStr

        // find main
        var qname = '$main';
        var dot = qname.indexOf('.');
        var type = qname;
        if (dot < 0) qname += '.main';
        else type = qname.substring(0, dot)
        var main = fan.std.Slot.findMethod(qname);

        // invoke main
        if (main.isStatic()) main.call();
        else main.callOn(fan.std.Type.find(type).make());
      }, false);
      </script>")
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  internal const static Int CR  := '\r'
  internal const static Int LF  := '\n'
  internal const static Int HT  := '\t'
  internal const static Int SP  := ' '
  internal const static Int maxTokenSize := 16384

}

**************************************************************************
** ChunkInStream
**************************************************************************
@Js
internal class ChunkInStream : InStream
{
  override Endian endian { set{ in.endian = it } get{ in.endian } }
  override Charset charset { set{ in.charset = it } get { in.charset } }

  new make(InStream in, Int? fixed := null) : super()
  {
    this.in = in
    this.noMoreChunks = (fixed != null)
    this.chunkRem     = (fixed != null) ? fixed : -1
  }

  override Int read()
  {
    if (pushback != null && !pushback.isEmpty) return pushback.pop
    if (!checkChunk) return -1
    chunkRem -= 1
    return in.read
  }

  //override Int avail() { throw UnsupportedErr("$this.typeof") }

  override Bool close() { return true }

  override Int readBytes(Array<Int8> ba, Int off := 0, Int len := ba.size) {
    if (pushback != null && !pushback.isEmpty && len > 0)
    {
      ba[0] = pushback.pop
      return 1
    }
    numRead := in.readBytes(ba, off, chunkRem.min(len))
    if (numRead != -1) chunkRem -= numRead
    return numRead
  }

  override Int readBuf(Buf buf, Int n)
  {
    if (pushback != null && !pushback.isEmpty && n > 0)
    {
      buf.write(pushback.pop)
      return 1
    }
    if (!checkChunk) return -1
    numRead := in.readBuf(buf, chunkRem.min(n))
    if (numRead != -1) chunkRem -= numRead
    return numRead
  }

  override This unread(Int b)
  {
    if (pushback == null) pushback = Int[,]
    pushback.push(b)
    return this
  }

  private Bool checkChunk()
  {
    try
    {
      // if we have bytes remaining in this chunk return true
      if (chunkRem > 0) return true

      // if we have set noMoreChunks this means we at end of the
      // logical chunked stream
      if (noMoreChunks) return false

      // we expect \r\n unless this is first chunk
      if (chunkRem != -1 && !WebUtil.readLine(in).isEmpty) throw Err()

      // read the next chunk status line
      line := WebUtil.readLine(in)
      semi := line.index(";")
      if (semi != null) line = line[0..semi]
      chunkRem = line.trim.toInt(16)

      // if we have more chunks keep chugging
      if (chunkRem > 0) return true

      // we are done so read trailing headers and set noMoreChunks
      // flag so that additional reads to this input stream
      // always are at end of stream
      noMoreChunks = true
      WebUtil.parseHeaders(in)
      return false
    }
    catch (Err e)
    {
      throw IOErr("Invalid format for HTTP chunked transfer encoding")
    }
  }

  override Str toStr() { "$this.typeof { noMoreChunks=$noMoreChunks chunkRem=$chunkRem pushback=$pushback }" }

  InStream in         // underlying input stream
  Bool noMoreChunks   // don't attempt to read more chunks
  Int chunkRem        // remaining bytes in current chunk (-1 for first chunk)
  Int[]? pushback     // stack for unread
}

**************************************************************************
** FixedOutStream
**************************************************************************
@Js
internal class FixedOutStream : OutStream
{
  new make(OutStream out, Int fixed) : super()
  {
    this.out = out
    this.fixed = fixed
  }

  override This write(Int b)
  {
    checkChunk(1)
    out.write(b)
    return this
  }

  override This writeBuf(Buf buf, Int n := buf.remaining)
  {
    checkChunk(n)
    out.writeBuf(buf, n)
    return this
  }

  override This flush()
  {
    out.flush
    return this
  }

  override Bool close()
  {
    try
    {
      this.flush
      return true
    }
    catch (Err e) return false
  }

  private Void checkChunk(Int n)
  {
    written += n
    if (written > fixed) throw IOErr("Attempt to write more than Content-Length: $fixed")
  }

  override This writeBytes(Array<Int8> ba, Int off := 0, Int len := ba.size) {
    checkChunk(len)
    out.writeBytes(ba, off, len)
    return this
  }

  override Endian endian {
    get { out.endian }
    set { out.endian = it }
  }
  override Charset charset {
    get { out.charset }
    set { out.charset = it }
  }

  override This sync() { out.sync; return this }

  OutStream out      // underlying output stream
  Int? fixed         // if non-null, then we're using as one fixed chunk
  Int written        // number of bytes written in this chunk
}

**************************************************************************
** ChunkOutStream
**************************************************************************
@Js
internal class ChunkOutStream : OutStream
{
  new make(OutStream out) : super()
  {
    this.out = out
    this.buffer = Buf(chunkSize + 256)
  }

  override This write(Int b)
  {
    buffer.write(b)
    checkChunk
    return this
  }

  override This writeBuf(Buf buf, Int n := buf.remaining)
  {
    buffer.writeBuf(buf, n)
    checkChunk
    return this
  }

  override This flush()
  {
    if (closed) throw IOErr("ChunkOutStream is closed")
    if (buffer.size > 0)
    {
      out.print(buffer.size.toHex).print("\r\n")
      out.writeBuf(buffer.flip, buffer.remaining)
      out.print("\r\n").flush
      buffer.clear
    }
    return this
  }

  override Bool close()
  {
    // never write end of chunk more than once
    if (closed) return true

    try
    {
      this.flush
      closed = true
      out.print("0\r\n\r\n").flush
      return true
    }
    catch return false
  }

  private Void checkChunk()
  {
    if (buffer.size >= chunkSize) flush
  }

  override This writeBytes(Array<Int8> ba, Int off := 0, Int len := ba.size) {
    buffer.out.writeBytes(ba, off, len)
    checkChunk
    return this
  }

  override Endian endian {
    get { out.endian }
    set { out.endian = it }
  }
  override Charset charset {
    get { out.charset }
    set { out.charset = it }
  }

  override This sync() { out.sync; return this }

  const static Int chunkSize := 1024

  OutStream out    // underlying output stream
  Buf? buffer      // buffer for bytes
  Bool closed      // have we written final close chunk?
}

**************************************************************************
** MultiPartInStream
**************************************************************************
@Js
internal class MultiPartInStream : InStream
{
  new make(InStream in, Str boundary) : super()
  {
    this.in = in
    this.boundary = boundary
    this.curLine = Buf(1024)
  }

  override Int read()
  {
    if (pushback != null && !pushback.isEmpty) return pushback.pop
    if (!checkLine) return -1
    numRead += 1
    return curLine.read
  }

  override Endian endian { set{ in.endian = it } get{ in.endian } }
  override Charset charset { set{ in.charset = it } get { in.charset } }

  //override Int avail() { throw UnsupportedErr("$this.typeof") }
  override Bool close() { return true }

  override Int readBytes(Array<Int8> ba, Int off := 0, Int len := ba.size) {
    if (pushback != null && !pushback.isEmpty && len > 0)
    {
      ba[0] = pushback.pop
      numRead += 1
      return 1
    }
    if (!checkLine) return -1
    actualRead := curLine.in.readBytes(ba, off, len)
    numRead += actualRead
    return actualRead
  }

  override Int readBuf(Buf buf, Int n)
  {
    if (pushback != null && !pushback.isEmpty && n > 0)
    {
      buf.write(pushback.pop)
      numRead += 1
      return 1
    }
    if (!checkLine) return -1
    actualRead := curLine.readBuf(buf, n)
    numRead += actualRead
    return actualRead
  }

  override This unread(Int b)
  {
    if (pushback == null) pushback = Int[,]
    pushback.push(b)
    numRead -= 1
    return this
  }

  private Bool checkLine()
  {
    // if we have bytes remaining in this line return true
    if (curLine.remaining > 0) return true

    // if we have read boundary, then this part is complete
    if (endOfPart) return false

    // read the next line or 1000 bytes into curLine buf
    curLine.clear
    for (i:=0; i<1024; ++i)
    {
      c := in.readU1
      curLine.write(c)
      if (c == '\n') break
    }

    // if not a property \r\n newline then keep chugging
    if (curLine.size < 2 || curLine[-2] != '\r') { curLine.seek(0); return true }

    // go ahead and keep reading as long as we have boundary match
    for (i:=0; i<boundary.size; ++i)
    {
      c := in.readU1
      if (c != boundary[i])
      {
        if (c == '\r') in.unread(c)
        else curLine.write(c)
        curLine.seek(0)
        return true
      }
      curLine.write(c)
    }

    // we have boundary match, so now figure out if end of parts
    curLine.size = curLine.size - boundary.size - 2
    c1 := in.readU1
    c2 := in.readU1
    if (c1 == '-' && c2 == '-')
    {
      endOfParts = true
      c1 = in.readU1
      c2 = in.readU1
    }
    if (c1 != '\r' || c2 != '\n') throw IOErr("Fishy boundary " + (c1.toChar + c2.toChar).toCode('"', true))
    endOfPart = true
    curLine.seek(0)
    return curLine.size > 0
  }

  InStream in
  Str boundary
  Buf curLine
  Int[]? pushback     // stack for unread
  Bool endOfPart
  Bool endOfParts
  Int numRead
}