// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Gzip for responses a client accepts it for, as Starlette's `GZipMiddleware`: a body of at
/// least `minimum` octets, sent in one message, not already content-encoded. `level` is the
/// zlib effort, 0 to 9.
///
/// A body sent in several messages is passed through as it comes: compressing it would hold it
/// back until the end, and a stream is sent in pieces so that it is not held back. An event
/// stream is never compressed, for the same reason.
pub(all) struct Gzip {
  minimum : Int32
  level : Int32
} derive(Eq, Debug)

///|
pub extend Gzip with Eq::{equal, not_equal}

///|
pub extend Gzip with Debug::{to_repr}

///|
/// Starlette's defaults: 500 octets and up, the most effort.
pub let gzip : Gzip = { minimum: 500, level: 9, }

///|
pub fn Gzip::new(
  minimum~ : Int32 = gzip.minimum,
  level~ : Int32 = gzip.level,
) -> Gzip {
  { minimum, level, }
}

///|
fn accepts_gzip(headers : Array[(String, String)]) -> Bool {
  header(headers, "accept-encoding") is Some(v) && v.to_lower().contains("gzip")
}

///|
fn Gzip::worth(
  self : Gzip,
  headers : Array[(String, String)],
  body : Bytes,
  more : Bool,
) -> Bool {
  !more &&
  body.length() >= self.minimum &&
  header(headers, "content-encoding") is None &&
  !(header(headers, "content-type") is Some(t) &&
  t.has_prefix("text/event-stream"))
}

///|
pub impl Layer for Gzip with fn wrap(self, app) {
  (scope, receive, send) => {
    guard scope is Http(h) && accepts_gzip(h.headers) else {
      app(scope, receive, send)
      return
    }
    // The start waits for the first body message, which decides whether to compress.
    let held : Ref[@spec.Event?] = Ref(None)
    let decided = Ref(false)
    app(scope, receive, event => {
      if decided.val {
        send(event)
        return
      }
      match (event, held.val) {
        (HttpResponseStart(trailers=false, ..), _) => held.val = Some(event)
        (
          HttpResponseBody(body~, more_body~),
          Some(HttpResponseStart(status~, headers~, trailers~)),
        ) => {
          decided.val = true
          held.val = None
          if self.worth(headers, body, more_body) {
            let packed = @gzip.gzip(body[:], level=self.level)
            let headers = put(headers, "content-encoding", "gzip")
              |> put("content-length", packed.length().to_string())
              |> vary("Accept-Encoding")
            send(HttpResponseStart(status~, headers~, trailers~))
            send(HttpResponseBody(body=packed, more_body=false))
          } else {
            send(HttpResponseStart(status~, headers~, trailers~))
            send(event)
          }
        }
        (other, Some(start)) => {
          decided.val = true
          held.val = None
          send(start)
          send(other)
        }
        (other, None) => send(other)
      }
    })
    if held.val is Some(start) {
      send(start)
    }
  }
}

///|
pub extend Gzip with Layer::{wrap}