//
// 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

**
** WispActor
**
internal const class WispActor : Actor
{

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

  new make(WispService service)
    : super(service.processorPool)
  {
    this.service = service
  }

//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////

  **
  ** Process a series of HTTP requests and responses on a socket.
  **
  override Obj? receive(Obj? msg)
  {
    process(((Unsafe)msg).val)
    return null
  }

  **
  ** Process a single HTTP request/response.
  **
  private Void process(TcpSocket socket)
  {
    WispRes? res
    WispReq? req
    close := true
    init := false

    try
    {
      // upgrade to TLS
      if (socket.localPort == service.httpsPort)
      {
        socket = socket.upgradeTls
      }

      // allocate request, response
      res = WispRes(service, socket)
      req = WispReq(service, socket, res)
      res.req = req

      // init thread locals
      Actor.locals["web.req"] = req
      Actor.locals["web.res"] = res

      // before we do anything set a tight receive timeout in case
      // the client fails to send us data in a timely fashion
      socket.options.receiveTimeout = 10sec

      // parse request line and headers, on error terminate processing
      if (!parseReq(req)) return

      // initialize the req and res
      initReqRes(req, res)
      init = true

      // service the request which runs thru the installed web steps
      service.root.onService

      // save session if accessed
      service.sessionStore.doSave

      // on upgraded to new protocol then do not close socket;
      // otherwise ensure response if committed and flushed
       if (res.upgraded)
         close = false
       else
         res.close
    }
    catch (Err e)
    {
      if (init)
        internalServerErr(req, res, e)
      else if (e is IOErr && e.msg.contains("javax.net.ssl."))
      {
        // only log JAVA SSL exceptions at debug level
        if (WispService.log.isDebug) WispService.log.debug("TLS Error", e)
      }
      else
        e.trace
    }
    finally
    {
      Actor.locals.remove("web.req")
      Actor.locals.remove("web.res")
      if (close) try { socket.close } catch {}
    }
  }

  private Bool isTls() { service.httpsPort != null }

//////////////////////////////////////////////////////////////////////////
// Request
//////////////////////////////////////////////////////////////////////////

  **
  ** Parse the first request line and request headers.
  ** Return true on success, false on failure.
  **
  internal static Bool parseReq(WispReq req)
  {
    try
    {
      // skip leading CRLF (4.1)
      in := req.socket.in
      line := WebUtil.readLine(in)
      while (line.isEmpty) line = WebUtil.readLine(in)

      // parse request-line (5.1)
      toks   := line.split
      method := toks[0]
      uri    := toks[1]
      ver    := toks[2]

      // method
      req.setMethod(method)

      // uri; immediately reject any uri which looks dangerous
      req.uri = Uri.decode(uri)
      if (req.uri.path.first == "..") throw Err("Reject URI")
      if (req.uri.pathStr.contains("//")) throw Err("Reject URI")

      // version
      if (ver == "HTTP/1.1") req.version = ver11
      else if (ver == "HTTP/1.0") req.version = ver10
      else throw Err("Unsupported version")

      // parse headers
      req.headers = WebUtil.parseHeaders(in).ro

      // success
      return true
    }
    catch (Err e)
    {
      // attempt to return error response
      try
      {
        out := req.socket.out
        req.socket.out
          .print("HTTP/1.1 400 Bad Request: $e.toStr.toCode\r\n")
          .print("\r\n").flush
      }
      catch (Err e2) {}
      return false
    }
  }

//////////////////////////////////////////////////////////////////////////
// Response
//////////////////////////////////////////////////////////////////////////

  **
  ** Initialize the request and response.
  **
  private Void initReqRes(WispReq req, WispRes res)
  {
    // init request input stream to read content
    req.webIn = initReqInStream(req)

    // configure Locale.cur for best match based on request
    Locale.setCur(req.locales.first)
  }

  **
  ** Map the raw HTTP input stream to handle the charset and transfer encoding
  **
  private InStream? initReqInStream(WispReq req)
  {
    // raw socket input stream
    raw := req.socket.in

    // if requesting an upgrade, then leave access to raw socket
    if (req.isUpgrade) return raw

    // init request - create content input stream wrapper
    wrap := WebUtil.makeContentInStream(req.headers, raw)

    // if the WebUtil didn't wrap the stream, then that means no
    // Content-Length or Transfer-Encoding - which in turn means we don't
    // consider this a valid request for sending a body in the request
    // according to 4.4 (since pipeling would be undefined)
    if (wrap === raw) return null
    return wrap
  }

//////////////////////////////////////////////////////////////////////////
// Error Handling
//////////////////////////////////////////////////////////////////////////

  **
  ** Send back 500 Internal server error.
  **
  private Void internalServerErr(WispReq req, WispRes res, Err err)
  {
    try
    {
      // if the error is that the socket has been disconnected
      // by the remote side, then this isn't *my* error so we don't
      // want to log spurious socket errors; we can detect
      // this by attempting to flush the socket
      if (err is IOErr)
      {
        try { req.socket.out.flush } catch { return }
      }

      // log internal error
      if (!err.msg.contains("Broken pipe"))
        WispService.log.err("Internal error processing: $req.uri", err)

      // if not committed yet, then return 400 if bad
      // client request or 500 if server error
      if (!res.isCommitted)
      {
        res.statusCode = 500
        res.headers.clear
        req.stash["err"] = err
        service.errMod.onService
        res.close
      }
    }
    catch (Err e) WispService.log.err("internalServiceError res failed", e)
  }

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

  static const Version ver10 := Version("1.0")
  static const Version ver11 := Version("1.1")
  static const Str wispVer   := "Wisp/" + WispActor#.pod.version

  const WispService service
}