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

using concurrent
using inet
using web

**
** WispRes
**
internal class WispRes : WebRes
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  new make(WispService service, TcpSocket socket)
  {
    // init headers
    headers := Str:Str[:] { caseInsensitive = true }
    headers["Date"] = DateTime.now.toHttpStr
    headers["Connection"] = "close"
    headers.setAll(service.extraResHeaders)

    this.service = service
    this.socket  = socket
    this.headers = headers
  }

//////////////////////////////////////////////////////////////////////////
// WebRes
//////////////////////////////////////////////////////////////////////////

  **
  ** Get or set the HTTP status code for this response. Status code
  ** defaults to 200. If response has already been committed, throws Err.
  ** If status code passed in is not recognized, throws Err.
  **
  override Int statusCode := 200
  {
    set
    {
      checkUncommitted
      &statusCode = it
    }
  }

  **
  ** Reason phrase to include in HTTP response line.  If null, then
  ** a status phrase is used based on the `statusCode`.
  **
  override Str? statusPhrase
  {
    set
    {
      checkUncommitted
      &statusPhrase = it
    }
  }

  **
  ** Map of HTTP response headers.  You must set all headers before
  ** you access out() for the first time, which commits the response.
  ** The headers are readonly once the response is committed.
  **
  override Str:Str headers

  **
  ** Get the list of cookies to set via a header fields.  Add a
  ** a Cookie to this list to set a cookie.  Throw an err if
  ** response is already committed.
  **
  override Cookie[] cookies := Cookie[,]
  {
    get { checkUncommitted; return &cookies }
  }

  **
  ** Return true if this response has been commmited.  A committed
  ** response has written its response headers, and can no longer
  ** modify its status code or headers.  A response is committed the
  ** first time that `out` is called.
  **
  override Bool isCommitted := false { private set }

  **
  ** Return the WebOutStream for this response.  The first time this
  ** method is accessed the response is committed: all headers
  ** currently set will be written to the stream, and can no longer
  ** be modified.  If the "Content-Length" header defines a fixed
  ** number of bytes, then attemps to write too many bytes will throw
  ** an IOErr.  If "Content-Length" is not defined, then a chunked
  ** transfer encoding is automatically used.
  **
  override WebOutStream out()
  {
    // if we are grabbing a stream to write response content, then
    // ensure we are committed with content; it is an illegal state
    // if another code path committed with no-content
    commit(true)
    if (webOut == null) throw Err("Must set Content-Length or Content-Type to write content")
    return webOut
  }

  **
  ** Send a redirect response to the client using the specified status
  ** code and url.  If this response has already been committed this
  ** method throws an Err.
  **
  override Void redirect(Uri uri, Int statusCode := 303)
  {
    checkUncommitted
    this.statusCode = statusCode
    headers["Location"] = uri.encode
    headers["Content-Length"] = "0"
    commit(false)
    done
  }

  **
  ** Send an error response to client using the specified status and
  ** HTML formatted message.  If this response has already been committed
  ** this method throws an Err.
  **
  override Void sendErr(Int statusCode, Str? msg := null)
  {
    checkUncommitted

    // unless content-length was forced to zero, write simple body
    Buf? buf := null
    if (headers["Content-Length"] == null)
    {
      buf = Buf()
      WebOutStream bufOut := WebOutStream(buf.out)
      bufOut.docType
      bufOut.html
      bufOut.head.title.w("$statusCode ${statusMsg[statusCode]}").titleEnd.headEnd
      bufOut.body
      bufOut.h1.w(statusMsg[statusCode]).h1End
      if (msg != null) bufOut.w(msg.toXml).nl
      bufOut.bodyEnd
      bufOut.htmlEnd

      headers["Content-Type"] = "text/html; charset=UTF-8"
      headers["Content-Length"] = buf.size.toStr
    }

    // write response
    this.statusCode = statusCode
    this.statusPhrase = msg
    if (buf != null) this.out.writeBuf(buf.flip)
    else commit(false)
    done
  }

  **
  ** Send an 100 Continue message to client which is used when the
  ** client specifies the "Expect: 100-continue" request header.
  **
  internal Void sendContinue()
  {
    checkUncommitted
    sout := socket.out
    sout.print("HTTP/1.1 100 Continue\r\n")
    sout.print("\r\n").flush
  }

  **
  ** Return if this response is complete - see `done`.
  **
  override Bool isDone := false { private set }

  **
  ** Done is called to indicate that that response is complete
  ** to terminate pipeline processing.  Once called, no further
  ** WebSteps in the pipeline are executed.
  **
  override Void done() { isDone = true }

  **
  ** Write response to socket, then and return ownership of socket
  ** to upgrade to different protocol.
  **
  override TcpSocket upgrade(Int statusCode := 101)
  {
    checkUncommitted
    this.statusCode = statusCode
    upgraded = true
    commit(false)
    return socket
  }

//////////////////////////////////////////////////////////////////////////
// Impl
//////////////////////////////////////////////////////////////////////////

  **
  ** If the response has already been committed, then throw an Err.
  **
  internal Void checkUncommitted()
  {
    if (isCommitted) throw Err("WebRes already committed")
  }

  **
  ** If we haven't committed yet, then write the response header.
  ** The content flag specifies whether this response will have a
  ** content body in the response.
  **
  internal Void commit(Bool content)
  {
    // check if committed
    if (isCommitted) return
    isCommitted = true

    // if we have content then we need to ensure we have our
    // headers and response stream are setup correctly
    sout := socket.out
    if (content) webOut = req.mod.makeResOut(sout)

    // lock down the headers
    headers = headers.ro

    // write response line and headers
    sout.print("HTTP/1.1 ").print(statusCode).print(" ").print(toStatusMsg).print("\r\n")
    WebUtil.writeHeaders(sout, headers)
    &cookies.each |Cookie c| { sout.print("Set-Cookie: ").print(c).print("\r\n") }
    sout.print("\r\n").flush
  }

  private Str toStatusMsg()
  {
    // special temp hook for WebSocket
    if (statusCode == 101 && &headers["Upgrade"] == "WebSocket")
      return "Web Socket Protocol Handshake"
    else if (statusPhrase != null)
      return statusPhrase
    else
      return statusMsg[statusCode] ?: statusCode.toStr
  }

  **
  ** This method is called to close down the response.  We ensure the
  ** response is committed and if we have a response output stream we
  ** close it to flush the content body.
  **
  internal Void close()
  {
    commit(false)
    if (webOut != null) webOut.close
  }

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

  internal WispService service
  internal WispReq? req
  internal TcpSocket socket
  internal WebOutStream? webOut
  internal Bool upgraded


}