// The response kinds that are an envelope rather than a body — FastAPI's
// `RedirectResponse` and `FileResponse`. `text` / `html` / `json` in `app.mbt`
// serialise a value; these two decide what the headers say about it.

///|
/// Whether `s` contains the ASCII code `c`.
fn holds_code(s : String, c : Int) -> Bool {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == c {
      return true
    }
  }
  false
}

///|
/// Percent-encode `s` over its UTF-8 bytes, leaving ASCII letters, digits,
/// `-._~` and anything in `safe` alone. Hex digits are upper case, as RFC 3986
/// §2.1 asks producers to emit them.
fn percent_encode(s : String, safe : String) -> String {
  let hex = "0123456789ABCDEF"
  let raw = @utf8.encode(s)
  let sb = StringBuilder()
  for i = 0; i < raw.length(); i = i + 1 {
    let b = raw[i].to_int()
    let unreserved = (b >= 0x41 && b <= 0x5A) ||
      (b >= 0x61 && b <= 0x7A) ||
      (b >= 0x30 && b <= 0x39) ||
      b == 0x2D ||
      b == 0x2E ||
      b == 0x5F ||
      b == 0x7E
    let allowed = unreserved || (b < 0x80 && holds_code(safe, b))
    if allowed {
      sb.write_char(b.unsafe_to_char())
    } else {
      sb.write_string("%")
      sb.write_char(hex[b / 16].unsafe_to_char())
      sb.write_char(hex[b % 16].unsafe_to_char())
    }
  }
  sb.to_string()
}

///|
/// A redirect to `url` (← FastAPI's `RedirectResponse`). The body is empty and
/// `Location` carries the target.
///
/// `307` is the default because it is the redirect that keeps the request's
/// method and body — the one a `POST` can safely follow, which `302` historically
/// is not. Use `303` to send a client to a `GET` after a write, and `301` / `308`
/// for a move that is permanent.
///
/// The URL is percent-encoded over the characters a URI reserves for structure,
/// so an already-encoded URL passes through unchanged and a `\r\n` smuggled into
/// one cannot open a header of its own.
pub fn redirect(url : String, status? : Int = 307) -> @moonasgi.Response {
  @moonasgi.Response::new(
    status,
    [("location", percent_encode(url, ":/%#?=@[]!$&'()*+,;"))],
    b"",
  )
}

///|
/// The media type a filename's extension implies, `application/octet-stream`
/// when nothing does. Deliberately short: this exists so `file_response` can
/// label the common downloads, not to be a copy of the IANA registry.
fn media_type_of(filename : String) -> String {
  let lower = filename.to_lower()
  let mut dot = -1
  for i = 0; i < lower.length(); i = i + 1 {
    if lower[i] == '.' {
      dot = i
    }
  }
  if dot < 0 {
    return "application/octet-stream"
  }
  match lower[dot + 1:].to_owned() {
    "txt" | "text" | "log" => "text/plain; charset=utf-8"
    "html" | "htm" => "text/html; charset=utf-8"
    "css" => "text/css; charset=utf-8"
    "csv" => "text/csv; charset=utf-8"
    "md" => "text/markdown; charset=utf-8"
    "js" | "mjs" => "text/javascript; charset=utf-8"
    "json" => "application/json"
    "xml" => "application/xml"
    "pdf" => "application/pdf"
    "zip" => "application/zip"
    "gz" => "application/gzip"
    "wasm" => "application/wasm"
    "png" => "image/png"
    "jpg" | "jpeg" => "image/jpeg"
    "gif" => "image/gif"
    "webp" => "image/webp"
    "svg" => "image/svg+xml"
    "ico" => "image/vnd.microsoft.icon"
    "woff" => "font/woff"
    "woff2" => "font/woff2"
    "mp3" => "audio/mpeg"
    "mp4" => "video/mp4"
    _ => "application/octet-stream"
  }
}

///|
/// The `Content-Disposition` value naming `filename`. A name that survives
/// percent-encoding unchanged is quoted as-is; anything else — a space, an
/// accent, a quote — is sent as RFC 6266's `filename*`, which is the only form
/// that can carry non-ASCII.
fn disposition_of(filename : String, inline : Bool) -> String {
  let kind = if inline { "inline" } else { "attachment" }
  let quoted = percent_encode(filename, "/")
  if quoted == filename {
    kind + "; filename=\"" + filename + "\""
  } else {
    kind + "; filename*=utf-8''" + quoted
  }
}

///|
/// A file download built from bytes already in hand (← FastAPI's `FileResponse`).
///
/// It takes the content rather than a path because moonapi has no filesystem of
/// its own — the same app runs on wasm, js and native, and only the server knows
/// how to read a file on any of them. What this adds is the envelope: a media
/// type guessed from `filename`'s extension unless `media_type` names one,
/// `Content-Length`, and a `Content-Disposition` that tells the browser to save
/// the file (or, with `inline`, to display it) under that name. An empty
/// `filename` leaves the disposition off entirely.
pub fn file_response(
  content : Bytes,
  filename? : String = "",
  media_type? : String = "",
  status? : Int = 200,
  inline? : Bool = false,
) -> @moonasgi.Response {
  let ct = if media_type != "" { media_type } else { media_type_of(filename) }
  let headers : Array[(String, String)] = [
    ("content-type", ct),
    ("content-length", content.length().to_string()),
  ]
  if filename != "" {
    headers.push(("content-disposition", disposition_of(filename, inline)))
  }
  @moonasgi.Response::new(status, headers, content)
}