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

**
** FileWeblet is used to service an HTTP request on a `sys::File`.
** It handles all the dirty details for cache control, compression,
** modification time, ETags, etc.
**
** Default implementation uses gzip encoding if gzip is supported
** by the client and the file's MIME type has a "text" media type.
**
** Current implementation supports ETags and Modification time
** for cache validation.  It does not specify any cache control
** directives.
**
class FileWeblet : Weblet
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  **
  ** Constructor with file to service.
  **
  new make(File file)
  {
    if (file.isDir) throw ArgErr("FileWeblet cannot process dir")
    this.file = file
  }

//////////////////////////////////////////////////////////////////////////
// Access
//////////////////////////////////////////////////////////////////////////

  **
  ** The file being serviced by this FileWeblet.
  **
  const File file

  **
  ** Get the modified time of the file floored to 1 second
  ** which is the most precise that HTTP can deal with.
  **
  virtual DateTime modified()
  {
    return DateTime.fromTimePoint(file.modified).floor(1sec)
  }

  **
  ** Compute the ETag for the file being serviced which uniquely
  ** identifies the file version.  The default implementation is
  ** a hash of the modified time and the file size.  The result
  ** of this method must conform to the ETag syntax and be
  ** wrapped in quotes.
  **
  virtual Str etag()
  {
    return "\"" + file.size.toHex + "-" + file.modified.toMillis.toHex + "\""
  }

  **
  ** Checks if the file being served is under the given directory.
  ** If it is not, a 404 response is immediately sent, short-circuiting
  ** any further attempts to serve the file.
  **
  **   FileWeblet(file).checkUnderDir(dir).onService
  **
  This checkUnderDir(File dir)
  {
    if (!dir.isDir) throw ArgErr("Not a directory: $dir")
    if (!file.normalize.pathStr.startsWith(dir.normalize.pathStr)) res.sendErr(404)
    return this
  }

  **
  ** Extra response headers to add for all 3xx and 2xx responses
  **
  [Str:Str]? extraResHeaders

//////////////////////////////////////////////////////////////////////////
// Weblet
//////////////////////////////////////////////////////////////////////////

  override Void onService()
  {
    if (res.isDone) return
    Weblet.super.onService
  }

  **
  ** Handle GET request for the file.
  **
  override Void onGet()
  {
    // if file doesn't exist
    if (!file.exists) { res.sendErr(404); return }

    // set identity headers
    res.headers["ETag"] = etag
    res.headers["Last-Modified"] = modified.toHttpStr

    // extra headers
    if (extraResHeaders != null)
      res.headers.setAll(extraResHeaders)

    // check if we can return a 304 not modified
    if (checkNotModified) return

    // MIME type
    mime := file.mimeType
    if (mime != null) res.headers["Content-Type"] = mime.toStr

    // check if client supports gzip and file has text/* MIME type
    // and if so send the file using gzip compression (we don't
    // know content length in this case)
    ae := req.headers["Accept-Encoding"] ?: ""
    if (isGzipFile(file) && WebUtil.parseQVals(ae)["gzip"] > 0f)
    {
      res.statusCode = 200
      res.headers["Content-Encoding"] = "gzip"
      out := Zip.gzipOutStream(res.out)
      file.in.pipe(out, file.size)
      out.close
      return
    }

    // service a normal 200 with no compression
    res.statusCode = 200
    res.headers["Content-Length"] = file.size.toStr
    file.in.pipe(res.out, file.size)
  }

  **
  ** Returns true if the file should be gzipped.
  **
  private static Bool isGzipFile(File file)
  {
    mime := file.mimeType
    if (mime == null) return false
    if (mime.mediaType == "text") return true
    if (mime.mediaType == "application")
    {
      if (mime.subType == "json") return true
    }
    if (mime.mediaType == "image")
    {
      if (mime.subType == "svg+xml") return true
    }
    return false
  }

  **
  ** Check if the request passed headers indicating it has
  ** cached version of the file.  If the file has not been
  ** modified, then service the request as 304 and return
  ** true.  This method supports ETag "If-None-Match" and
  ** "If-Modified-Since" modification time.
  **
  virtual protected Bool checkNotModified()
  {
    doCheckNotModified(req, res, etag, modified)
  }

  **
  ** Utility for standard check modified logic
  **
  internal static Bool doCheckNotModified(WebReq req, WebRes res, Str etag, DateTime modified)
  {
    // check If-Match-None
    matchNone := req.headers["If-None-Match"]
    if (matchNone != null)
    {
      match := WebUtil.parseList(matchNone).any |Str s->Bool|
      {
        return s == etag || s == "*"
      }
      if (match)
      {
        res.statusCode = 304
        return true
      }
    }

    // check If-Modified-Since
    since := req.headers["If-Modified-Since"]
    if (since != null)
    {
      sinceTime := DateTime.fromHttpStr(since, false)
      if (modified == sinceTime)
      {
        res.statusCode = 304
        return true
      }
    }

    // gotta do it the hard way
    return false
  }

}